mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-06 18:52:43 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4dc1c1f03 | |||
| 7181df0479 | |||
| 633e83a4ab | |||
| d745e6b656 | |||
| 59b5f8cbbe | |||
| b62e88c4ed | |||
| d6108a0722 | |||
| 1af7e59d88 | |||
| 7b425e9a9d | |||
| 596a03900b | |||
| b283644d95 | |||
| 808690c137 | |||
| 136c347586 | |||
| 258b9ec54e | |||
| e81238038e | |||
| fcf6964d7d | |||
| 54ca58e610 | |||
| 2131a65299 | |||
| 243bc7b0d0 | |||
| b8de041497 | |||
| 8287822354 | |||
| 0f83a292f6 | |||
| c738e35a8c | |||
| b2e1e24ca5 | |||
| c7f457da3e | |||
| bed3758268 | |||
| a1a923df94 | |||
| bbf324ea83 | |||
| adc4309951 | |||
| b8ab72a141 |
+6
-1
@@ -14,7 +14,10 @@ RUN apk update && \
|
|||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
curl \
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg
|
ffmpeg \
|
||||||
|
make \
|
||||||
|
python3 \
|
||||||
|
g++
|
||||||
|
|
||||||
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
||||||
COPY --from=build /client/dist /client/dist
|
COPY --from=build /client/dist /client/dist
|
||||||
@@ -23,6 +26,8 @@ COPY server server
|
|||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
RUN apk del make python3 g++
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
HEALTHCHECK \
|
HEALTHCHECK \
|
||||||
--interval=30s \
|
--interval=30s \
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
v-for="(episode, index) in episodesList"
|
v-for="(episode, index) in episodesList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
:class="itemEpisodeMap[episode.enclosure.url?.split('?')[0]] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
||||||
@click="toggleSelectEpisode(index, episode)"
|
@click="toggleSelectEpisode(index, episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="itemEpisodeMap[episode.enclosure.url?.split('?')[0]]" class="material-icons text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
selectAll: false,
|
selectAll: false,
|
||||||
search: null,
|
search: null,
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchText: null,
|
searchText: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
},
|
},
|
||||||
allDownloaded() {
|
allDownloaded() {
|
||||||
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url])
|
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]])
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
episodesSelected() {
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
@@ -108,17 +108,14 @@ export default {
|
|||||||
itemEpisodeMap() {
|
itemEpisodeMap() {
|
||||||
var map = {}
|
var map = {}
|
||||||
this.itemEpisodes.forEach((item) => {
|
this.itemEpisodes.forEach((item) => {
|
||||||
if (item.enclosure) map[item.enclosure.url] = true
|
if (item.enclosure) map[item.enclosure.url.split('?')[0]] = true
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
episodesList() {
|
episodesList() {
|
||||||
return this.episodes.filter((episode) => {
|
return this.episodes.filter((episode) => {
|
||||||
if (!this.searchText) return true
|
if (!this.searchText) return true
|
||||||
return (
|
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
(episode.title && episode.title.toLowerCase().includes(this.searchText)) ||
|
|
||||||
(episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -136,14 +133,14 @@ export default {
|
|||||||
toggleSelectAll(val) {
|
toggleSelectAll(val) {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) this.selectedEpisodes[String(i)] = false
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false
|
||||||
else this.$set(this.selectedEpisodes, String(i), val)
|
else this.$set(this.selectedEpisodes, String(i), val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkSetIsSelectedAll() {
|
checkSetIsSelectedAll() {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (!this.itemEpisodeMap[episode.enclosure.url] && !this.selectedEpisodes[String(i)]) {
|
if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) {
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,7 +148,7 @@ export default {
|
|||||||
this.selectAll = true
|
this.selectAll = true
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(index, episode) {
|
toggleSelectEpisode(index, episode) {
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) return
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
||||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
||||||
this.checkSetIsSelectedAll()
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
|
|
||||||
setHlsStream() {
|
setHlsStream() {
|
||||||
this.trackStartTime = 0
|
this.trackStartTime = 0
|
||||||
|
this.currentTrackIndex = 0
|
||||||
|
|
||||||
// iOS does not support Media Elements but allows for HLS in the native audio player
|
// iOS does not support Media Elements but allows for HLS in the native audio player
|
||||||
if (!Hls.isSupported()) {
|
if (!Hls.isSupported()) {
|
||||||
|
|||||||
+21
-21
@@ -20,8 +20,8 @@
|
|||||||
"ButtonCreate": "Crea",
|
"ButtonCreate": "Crea",
|
||||||
"ButtonCreateBackup": "Crea un Backup",
|
"ButtonCreateBackup": "Crea un Backup",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Elimina",
|
||||||
"ButtonDownloadQueue": "Queue",
|
"ButtonDownloadQueue": "Coda",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Modifica",
|
||||||
"ButtonEditChapters": "Modifica Capitoli",
|
"ButtonEditChapters": "Modifica Capitoli",
|
||||||
"ButtonEditPodcast": "Modifica Podcast",
|
"ButtonEditPodcast": "Modifica Podcast",
|
||||||
"ButtonForceReScan": "Forza Re-Scan",
|
"ButtonForceReScan": "Forza Re-Scan",
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAll": "Tutti",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
"LabelAlreadyInYourLibrary": "Already in your library",
|
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
|
||||||
"LabelAppend": "Appese",
|
"LabelAppend": "Appese",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
"LabelInProgress": "In Corso",
|
"LabelInProgress": "In Corso",
|
||||||
"LabelInterval": "Intervallo",
|
"LabelInterval": "Intervallo",
|
||||||
"LabelIntervalCustomDailyWeekly": "Personalizza giorni/settimane",
|
"LabelIntervalCustomDailyWeekly": "Personalizza giorni/settimane",
|
||||||
"LabelIntervalEvery12Hours": "EOgni 12 Ore",
|
"LabelIntervalEvery12Hours": "Ogni 12 Ore",
|
||||||
"LabelIntervalEvery15Minutes": "Ogni 15 Minuti",
|
"LabelIntervalEvery15Minutes": "Ogni 15 Minuti",
|
||||||
"LabelIntervalEvery2Hours": "Ogni 2 Ore",
|
"LabelIntervalEvery2Hours": "Ogni 2 Ore",
|
||||||
"LabelIntervalEvery30Minutes": "Ogni 30 Minuti",
|
"LabelIntervalEvery30Minutes": "Ogni 30 Minuti",
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
"LabelNewestAuthors": "Autori Recenti",
|
"LabelNewestAuthors": "Autori Recenti",
|
||||||
"LabelNewestEpisodes": "Episodi Recenti",
|
"LabelNewestEpisodes": "Episodi Recenti",
|
||||||
"LabelNewPassword": "Nuova Password",
|
"LabelNewPassword": "Nuova Password",
|
||||||
"LabelNextBackupDate": "Next backup date",
|
"LabelNextBackupDate": "Data Prossimo Backup",
|
||||||
"LabelNextScheduledRun": "Next scheduled run",
|
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
||||||
"LabelNotes": "Note",
|
"LabelNotes": "Note",
|
||||||
"LabelNotFinished": "Da Completare",
|
"LabelNotFinished": "Da Completare",
|
||||||
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
"LabelNumberOfBooks": "Numero di libri",
|
"LabelNumberOfBooks": "Numero di libri",
|
||||||
"LabelNumberOfEpisodes": "# degli episodi",
|
"LabelNumberOfEpisodes": "# degli episodi",
|
||||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||||
"LabelOverwrite": "Overwrite",
|
"LabelOverwrite": "Sovrascrivi",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Percorso",
|
"LabelPath": "Percorso",
|
||||||
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
||||||
@@ -310,9 +310,9 @@
|
|||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Timo di Podcast",
|
||||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||||
"LabelProgress": "Cominciati",
|
"LabelProgress": "Cominciati",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
@@ -320,14 +320,14 @@
|
|||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Raccomandati",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Rimuovi cover",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Aperto",
|
"LabelRSSFeedOpen": "RSS Feed Aperto",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Ricerca",
|
"LabelSearchTerm": "Ricerca",
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
|
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
|
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Formato Ora",
|
||||||
"LabelShowAll": "Mostra Tutto",
|
"LabelShowAll": "Mostra Tutto",
|
||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTimeListened": "Tempo di Ascolto",
|
"LabelTimeListened": "Tempo di Ascolto",
|
||||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||||
"LabelTimeRemaining": "{0} rimanente",
|
"LabelTimeRemaining": "{0} rimanente",
|
||||||
@@ -502,8 +502,8 @@
|
|||||||
"MessageNoCollections": "Nessuna Raccolta",
|
"MessageNoCollections": "Nessuna Raccolta",
|
||||||
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
||||||
"MessageNoDescription": "Nessuna descrizione",
|
"MessageNoDescription": "Nessuna descrizione",
|
||||||
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
"MessageNoDownloadsInProgress": "Nessun download attualmente in corso",
|
||||||
"MessageNoDownloadsQueued": "No downloads queued",
|
"MessageNoDownloadsQueued": "Nessuna coda di download",
|
||||||
"MessageNoEpisodeMatchesFound": "Nessun episodio corrispondente trovato",
|
"MessageNoEpisodeMatchesFound": "Nessun episodio corrispondente trovato",
|
||||||
"MessageNoEpisodes": "Nessun Episodio",
|
"MessageNoEpisodes": "Nessun Episodio",
|
||||||
"MessageNoFoldersAvailable": "Nessuna Cartella disponibile",
|
"MessageNoFoldersAvailable": "Nessuna Cartella disponibile",
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||||
"MessageNoSeries": "Nessuna Serie",
|
"MessageNoSeries": "Nessuna Serie",
|
||||||
"MessageNoTags": "No Tags",
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNoTasksRunning": "No Tasks Running",
|
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
||||||
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
||||||
"PlaceholderSearch": "Cerca..",
|
"PlaceholderSearch": "Cerca..",
|
||||||
"PlaceholderSearchEpisode": "Search episode..",
|
"PlaceholderSearchEpisode": "Cerca Episodio..",
|
||||||
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
||||||
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
||||||
"ToastAuthorImageRemoveFailed": "Rimozione immagine autore Fallita",
|
"ToastAuthorImageRemoveFailed": "Rimozione immagine autore Fallita",
|
||||||
@@ -627,7 +627,7 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
|
|||||||
Generated
+2360
-16
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,9 @@
|
|||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
|
"sequelize": "^6.29.1",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
|
"sqlite3": "^5.1.4",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const { Sequelize } = require('sequelize')
|
||||||
|
|
||||||
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
constructor() {
|
||||||
|
this.sequelize = null
|
||||||
|
}
|
||||||
|
|
||||||
|
get models() {
|
||||||
|
return this.sequelize?.models || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(force = false) {
|
||||||
|
if (!await this.connect()) {
|
||||||
|
throw new Error('Database connection failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.buildModels(force)
|
||||||
|
Logger.info(`[Database] Db initialized`, Object.keys(this.sequelize.models))
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
const dbPath = Path.join(global.ConfigPath, 'database.sqlite')
|
||||||
|
Logger.info(`[Database] Initializing db at "${dbPath}"`)
|
||||||
|
this.sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: dbPath,
|
||||||
|
logging: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
this.sequelize.uppercaseFirst = str => str ? `${str[0].toUpperCase()}${str.substr(1)}` : ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sequelize.authenticate()
|
||||||
|
Logger.info(`[Database] Db connection was successful`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[Database] Failed to connect to db`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildModels(force = false) {
|
||||||
|
require('./models/User')(this.sequelize)
|
||||||
|
require('./models/FileMetadata')(this.sequelize)
|
||||||
|
require('./models/EBookFile')(this.sequelize)
|
||||||
|
require('./models/Book')(this.sequelize)
|
||||||
|
require('./models/Podcast')(this.sequelize)
|
||||||
|
require('./models/Library')(this.sequelize)
|
||||||
|
require('./models/LibraryFolder')(this.sequelize)
|
||||||
|
require('./models/LibraryItem')(this.sequelize)
|
||||||
|
require('./models/PodcastEpisode')(this.sequelize)
|
||||||
|
require('./models/MediaProgress')(this.sequelize)
|
||||||
|
require('./models/LibraryFile')(this.sequelize)
|
||||||
|
require('./models/Person')(this.sequelize)
|
||||||
|
require('./models/AudioBookmark')(this.sequelize)
|
||||||
|
require('./models/MediaFile')(this.sequelize)
|
||||||
|
require('./models/MediaStream')(this.sequelize)
|
||||||
|
require('./models/AudioTrack')(this.sequelize)
|
||||||
|
require('./models/BookAuthor')(this.sequelize)
|
||||||
|
require('./models/BookChapter')(this.sequelize)
|
||||||
|
require('./models/Genre')(this.sequelize)
|
||||||
|
require('./models/BookGenre')(this.sequelize)
|
||||||
|
require('./models/PodcastGenre')(this.sequelize)
|
||||||
|
require('./models/BookNarrator')(this.sequelize)
|
||||||
|
require('./models/Series')(this.sequelize)
|
||||||
|
require('./models/BookSeries')(this.sequelize)
|
||||||
|
require('./models/Tag')(this.sequelize)
|
||||||
|
require('./models/BookTag')(this.sequelize)
|
||||||
|
require('./models/PodcastTag')(this.sequelize)
|
||||||
|
require('./models/Collection')(this.sequelize)
|
||||||
|
require('./models/CollectionBook')(this.sequelize)
|
||||||
|
require('./models/Playlist')(this.sequelize)
|
||||||
|
require('./models/PlaylistMediaItem')(this.sequelize)
|
||||||
|
require('./models/Device')(this.sequelize)
|
||||||
|
require('./models/PlaybackSession')(this.sequelize)
|
||||||
|
require('./models/PlaybackSessionListenTime')(this.sequelize)
|
||||||
|
require('./models/Feed')(this.sequelize)
|
||||||
|
require('./models/FeedEpisode')(this.sequelize)
|
||||||
|
require('./models/Setting')(this.sequelize)
|
||||||
|
require('./models/LibrarySetting')(this.sequelize)
|
||||||
|
require('./models/Notification')(this.sequelize)
|
||||||
|
require('./models/UserPermission')(this.sequelize)
|
||||||
|
|
||||||
|
return this.sequelize.sync({ force })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = new Database()
|
||||||
+13
-2
@@ -8,7 +8,8 @@ const rateLimit = require('./libs/expressRateLimit')
|
|||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
const dbMigration = require('./utils/dbMigration')
|
const dbMigration2 = require('./utils/migrations/dbMigrationOld')
|
||||||
|
const dbMigration3 = require('./utils/migrations/dbMigration')
|
||||||
const filePerms = require('./utils/filePerms')
|
const filePerms = require('./utils/filePerms')
|
||||||
const fileUtils = require('./utils/fileUtils')
|
const fileUtils = require('./utils/fileUtils')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
@@ -17,8 +18,11 @@ const Auth = require('./Auth')
|
|||||||
const Watcher = require('./Watcher')
|
const Watcher = require('./Watcher')
|
||||||
const Scanner = require('./scanner/Scanner')
|
const Scanner = require('./scanner/Scanner')
|
||||||
const Db = require('./Db')
|
const Db = require('./Db')
|
||||||
|
const Database = require('./Database')
|
||||||
const SocketAuthority = require('./SocketAuthority')
|
const SocketAuthority = require('./SocketAuthority')
|
||||||
|
|
||||||
|
const routes = require('./routes/index')
|
||||||
|
|
||||||
const ApiRouter = require('./routers/ApiRouter')
|
const ApiRouter = require('./routers/ApiRouter')
|
||||||
const HlsRouter = require('./routers/HlsRouter')
|
const HlsRouter = require('./routers/HlsRouter')
|
||||||
const StaticRouter = require('./routers/StaticRouter')
|
const StaticRouter = require('./routers/StaticRouter')
|
||||||
@@ -82,6 +86,7 @@ class Server {
|
|||||||
|
|
||||||
// Routers
|
// Routers
|
||||||
this.apiRouter = new ApiRouter(this)
|
this.apiRouter = new ApiRouter(this)
|
||||||
|
|
||||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
||||||
this.staticRouter = new StaticRouter(this.db)
|
this.staticRouter = new StaticRouter(this.db)
|
||||||
|
|
||||||
@@ -99,13 +104,18 @@ class Server {
|
|||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
await this.playbackSessionManager.removeOrphanStreams()
|
await this.playbackSessionManager.removeOrphanStreams()
|
||||||
|
|
||||||
|
// TODO: Test new db connection
|
||||||
|
const force = false
|
||||||
|
await Database.init(force)
|
||||||
|
if (force) await dbMigration3.migrate()
|
||||||
|
|
||||||
const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
||||||
if (previousVersion) {
|
if (previousVersion) {
|
||||||
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
|
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
|
||||||
}
|
}
|
||||||
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
|
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
|
||||||
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
|
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
|
||||||
await dbMigration.migrate(this.db)
|
await dbMigration2.migrate(this.db)
|
||||||
} else {
|
} else {
|
||||||
await this.db.init()
|
await this.db.init()
|
||||||
}
|
}
|
||||||
@@ -162,6 +172,7 @@ class Server {
|
|||||||
// Static folder
|
// Static folder
|
||||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
|
router.use('/api/v1', routes)
|
||||||
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
||||||
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||||
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const itemDb = require('../db/item.db')
|
||||||
|
|
||||||
|
const getLibraryItem = async (req, res) => {
|
||||||
|
let libraryItem = null
|
||||||
|
if (req.query.minified == 1) {
|
||||||
|
libraryItem = await itemDb.getLibraryItemMinified(req.params.id)
|
||||||
|
} else if (req.query.expanded == 1) {
|
||||||
|
libraryItem = await itemDb.getLibraryItemExpanded(req.params.id)
|
||||||
|
} else {
|
||||||
|
libraryItem = await itemDb.getLibraryItemFull(req.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLibraryItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const libraryDb = require('../db/library.db')
|
||||||
|
const itemDb = require('../db/item.db')
|
||||||
|
|
||||||
|
const getAllLibraries = async (req, res) => {
|
||||||
|
const libraries = await libraryDb.getAllLibraries()
|
||||||
|
res.json({
|
||||||
|
libraries
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibrary = async (req, res) => {
|
||||||
|
const library = await libraryDb.getLibrary(req.params.id)
|
||||||
|
if (!library) return res.sendStatus(404)
|
||||||
|
res.json(library)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibraryItems = async (req, res) => {
|
||||||
|
const libraryItems = await itemDb.getLibraryItemsForLibrary(req.params.id)
|
||||||
|
res.json({
|
||||||
|
libraryItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAllLibraries,
|
||||||
|
getLibrary,
|
||||||
|
getLibraryItems
|
||||||
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
const { Sequelize } = require('sequelize')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
|
const getLibraryItemMinified = (libraryItemId) => {
|
||||||
|
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit',
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "audioTracks" WHERE "audioTracks"."mediaItemId" = book.id)'), 'numAudioTracks'],
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "bookChapters" WHERE "bookChapters"."bookId" = book.id)'), 'numChapters']
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'authors',
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'narrators',
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes',
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibraryItemFull = (libraryItemId) => {
|
||||||
|
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.audioTrack
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'authors',
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'narrators',
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.bookChapter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.eBookFile,
|
||||||
|
include: 'fileMetadata'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.podcastEpisode,
|
||||||
|
include: {
|
||||||
|
model: Database.models.audioTrack
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibraryItemExpanded = (libraryItemId) => {
|
||||||
|
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.fileMetadata,
|
||||||
|
as: 'imageFile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.audioTrack,
|
||||||
|
include: {
|
||||||
|
model: Database.models.mediaFile,
|
||||||
|
include: [
|
||||||
|
'fileMetadata',
|
||||||
|
'mediaStreams'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'authors',
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'narrators',
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.bookChapter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.eBookFile,
|
||||||
|
include: 'fileMetadata'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.fileMetadata,
|
||||||
|
as: 'imageFile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcastEpisode,
|
||||||
|
include: {
|
||||||
|
model: Database.models.audioTrack,
|
||||||
|
include: {
|
||||||
|
model: Database.models.mediaFile,
|
||||||
|
include: [
|
||||||
|
'fileMetadata',
|
||||||
|
'mediaStreams'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.libraryFile,
|
||||||
|
include: 'fileMetadata'
|
||||||
|
},
|
||||||
|
'libraryFolder',
|
||||||
|
'library'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibraryItemsForLibrary = async (libraryId) => {
|
||||||
|
return Database.models.libraryItem.findAll({
|
||||||
|
where: {
|
||||||
|
libraryId
|
||||||
|
},
|
||||||
|
limit: 50,
|
||||||
|
order: [
|
||||||
|
[Database.models.book, 'title', 'DESC'],
|
||||||
|
[Database.models.podcast, 'title', 'DESC']
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit',
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "audioTracks" WHERE "audioTracks"."mediaItemId" = book.id)'), 'numAudioTracks'],
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "bookChapters" WHERE "bookChapters"."bookId" = book.id)'), 'numChapters']
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'authors',
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.person,
|
||||||
|
as: 'narrators',
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes',
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.genre,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.tag,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLibraryItemMinified,
|
||||||
|
getLibraryItemFull,
|
||||||
|
getLibraryItemExpanded,
|
||||||
|
getLibraryItemsForLibrary
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
|
const getAllLibraries = () => {
|
||||||
|
return Database.models.library.findAll({
|
||||||
|
include: {
|
||||||
|
model: Database.models.librarySetting,
|
||||||
|
attributes: ['key', 'value']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibrary = (libraryId) => {
|
||||||
|
return Database.models.library.findByPk(libraryId, {
|
||||||
|
include: {
|
||||||
|
model: Database.models.librarySetting,
|
||||||
|
attributes: ['key', 'value']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAllLibraries,
|
||||||
|
getLibrary
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Book has many AudioBookmark. PodcastEpisode has many AudioBookmark.
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class AudioBookmark extends Model {
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioBookmark.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
time: DataTypes.INTEGER
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'audioBookmark'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user, book, podcastEpisode } = sequelize.models
|
||||||
|
book.hasMany(AudioBookmark, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AudioBookmark.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasMany(AudioBookmark, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AudioBookmark.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
AudioBookmark.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user.hasMany(AudioBookmark)
|
||||||
|
AudioBookmark.belongsTo(user)
|
||||||
|
|
||||||
|
return AudioBookmark
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Book has many AudioTrack. PodcastEpisode has one AudioTrack.
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class AudioTrack extends Model {
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioTrack.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
index: DataTypes.INTEGER,
|
||||||
|
startOffset: DataTypes.FLOAT,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
mimeType: DataTypes.STRING,
|
||||||
|
codec: DataTypes.STRING,
|
||||||
|
trackNumber: DataTypes.INTEGER,
|
||||||
|
discNumber: DataTypes.INTEGER
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'audioTrack'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, mediaFile } = sequelize.models
|
||||||
|
|
||||||
|
mediaFile.hasOne(AudioTrack)
|
||||||
|
AudioTrack.belongsTo(mediaFile)
|
||||||
|
|
||||||
|
book.hasMany(AudioTrack, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AudioTrack.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasOne(AudioTrack, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AudioTrack.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
AudioTrack.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return AudioTrack
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Book extends Model { }
|
||||||
|
|
||||||
|
Book.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
subtitle: DataTypes.STRING,
|
||||||
|
publishedYear: DataTypes.STRING,
|
||||||
|
publishedDate: DataTypes.STRING,
|
||||||
|
publisher: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
isbn: DataTypes.STRING,
|
||||||
|
asin: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
lastCoverSearchQuery: DataTypes.STRING,
|
||||||
|
lastCoverSearch: DataTypes.DATE
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'book'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fileMetadata, eBookFile } = sequelize.models
|
||||||
|
|
||||||
|
fileMetadata.hasOne(Book, { foreignKey: 'imageFileId' })
|
||||||
|
Book.belongsTo(fileMetadata, { as: 'imageFile', foreignKey: 'imageFileId' }) // Ref: https://sequelize.org/docs/v6/core-concepts/assocs/#defining-an-alias
|
||||||
|
|
||||||
|
eBookFile.hasOne(Book)
|
||||||
|
Book.belongsTo(eBookFile)
|
||||||
|
|
||||||
|
return Book
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookAuthor extends Model { }
|
||||||
|
|
||||||
|
BookAuthor.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookAuthor',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, person } = sequelize.models
|
||||||
|
book.belongsToMany(person, { through: BookAuthor, as: 'authors', otherKey: 'authorId' })
|
||||||
|
person.belongsToMany(book, { through: BookAuthor, foreignKey: 'authorId' })
|
||||||
|
|
||||||
|
book.hasMany(BookAuthor)
|
||||||
|
BookAuthor.belongsTo(book)
|
||||||
|
|
||||||
|
person.hasMany(BookAuthor, { foreignKey: 'authorId' })
|
||||||
|
BookAuthor.belongsTo(person, { as: 'author', foreignKey: 'authorId' })
|
||||||
|
|
||||||
|
return BookAuthor
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookChapter extends Model { }
|
||||||
|
|
||||||
|
BookChapter.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
index: DataTypes.INTEGER,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
start: DataTypes.FLOAT,
|
||||||
|
end: DataTypes.FLOAT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookChapter'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book } = sequelize.models
|
||||||
|
|
||||||
|
book.hasMany(BookChapter)
|
||||||
|
BookChapter.belongsTo(book)
|
||||||
|
|
||||||
|
return BookChapter
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookGenre extends Model { }
|
||||||
|
|
||||||
|
BookGenre.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookGenre',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, genre } = sequelize.models
|
||||||
|
book.belongsToMany(genre, { through: BookGenre })
|
||||||
|
genre.belongsToMany(book, { through: BookGenre })
|
||||||
|
|
||||||
|
book.hasMany(BookGenre)
|
||||||
|
BookGenre.belongsTo(book)
|
||||||
|
|
||||||
|
genre.hasMany(BookGenre)
|
||||||
|
BookGenre.belongsTo(genre)
|
||||||
|
|
||||||
|
return BookGenre
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookNarrator extends Model { }
|
||||||
|
|
||||||
|
BookNarrator.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookNarrator',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, person } = sequelize.models
|
||||||
|
book.belongsToMany(person, { through: BookNarrator, as: 'narrators', otherKey: 'narratorId' })
|
||||||
|
person.belongsToMany(book, { through: BookNarrator, foreignKey: 'narratorId' })
|
||||||
|
|
||||||
|
book.hasMany(BookNarrator)
|
||||||
|
BookNarrator.belongsTo(book)
|
||||||
|
|
||||||
|
person.hasMany(BookNarrator, { foreignKey: 'narratorId' })
|
||||||
|
BookNarrator.belongsTo(person, { as: 'narrator', foreignKey: 'narratorId' })
|
||||||
|
|
||||||
|
return BookNarrator
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookSeries extends Model { }
|
||||||
|
|
||||||
|
BookSeries.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
sequence: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookSeries',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, series } = sequelize.models
|
||||||
|
book.belongsToMany(series, { through: BookSeries })
|
||||||
|
series.belongsToMany(book, { through: BookSeries })
|
||||||
|
|
||||||
|
book.hasMany(BookSeries)
|
||||||
|
BookSeries.belongsTo(book)
|
||||||
|
|
||||||
|
series.hasMany(BookSeries)
|
||||||
|
BookSeries.belongsTo(series)
|
||||||
|
|
||||||
|
return BookSeries
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookTag extends Model { }
|
||||||
|
|
||||||
|
BookTag.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookTag',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, tag } = sequelize.models
|
||||||
|
book.belongsToMany(tag, { through: BookTag })
|
||||||
|
tag.belongsToMany(book, { through: BookTag })
|
||||||
|
|
||||||
|
book.hasMany(BookTag)
|
||||||
|
BookTag.belongsTo(book)
|
||||||
|
|
||||||
|
tag.hasMany(BookTag)
|
||||||
|
BookTag.belongsTo(tag)
|
||||||
|
|
||||||
|
return BookTag
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Collection extends Model { }
|
||||||
|
|
||||||
|
Collection.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'collection'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library } = sequelize.models
|
||||||
|
|
||||||
|
library.hasMany(Collection)
|
||||||
|
Collection.belongsTo(library)
|
||||||
|
|
||||||
|
return Collection
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class CollectionBook extends Model { }
|
||||||
|
|
||||||
|
CollectionBook.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
timestamps: true,
|
||||||
|
updatedAt: false,
|
||||||
|
modelName: 'collectionBook'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, collection } = sequelize.models
|
||||||
|
book.belongsToMany(collection, { through: CollectionBook })
|
||||||
|
collection.belongsToMany(book, { through: CollectionBook })
|
||||||
|
|
||||||
|
book.hasMany(CollectionBook)
|
||||||
|
CollectionBook.belongsTo(book)
|
||||||
|
|
||||||
|
collection.hasMany(CollectionBook)
|
||||||
|
CollectionBook.belongsTo(collection)
|
||||||
|
|
||||||
|
return CollectionBook
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Device extends Model { }
|
||||||
|
|
||||||
|
Device.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
identifier: DataTypes.STRING,
|
||||||
|
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
|
||||||
|
clientVersion: DataTypes.STRING,
|
||||||
|
ipAddress: DataTypes.STRING,
|
||||||
|
deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||||
|
deviceVersion: DataTypes.STRING // e.g. Browser version or Android SDK
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'device'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(Device)
|
||||||
|
Device.belongsTo(user)
|
||||||
|
|
||||||
|
return Device
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class EBookFile extends Model { }
|
||||||
|
|
||||||
|
EBookFile.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
format: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'eBookFile'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fileMetadata } = sequelize.models
|
||||||
|
|
||||||
|
fileMetadata.hasOne(EBookFile, { foreignKey: 'fileMetadataId' })
|
||||||
|
EBookFile.belongsTo(fileMetadata, { as: 'fileMetadata', foreignKey: 'fileMetadataId' })
|
||||||
|
|
||||||
|
return EBookFile
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Feeds can be created from LibraryItem, Collection, Playlist or Series
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Feed extends Model {
|
||||||
|
getEntity(options) {
|
||||||
|
if (!this.entityType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
slug: DataTypes.STRING,
|
||||||
|
entityType: DataTypes.STRING,
|
||||||
|
entityId: DataTypes.UUIDV4,
|
||||||
|
entityUpdatedAt: DataTypes.DATE,
|
||||||
|
serverAddress: DataTypes.STRING,
|
||||||
|
feedURL: DataTypes.STRING,
|
||||||
|
imageURL: DataTypes.STRING,
|
||||||
|
siteURL: DataTypes.STRING,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
podcastType: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
ownerName: DataTypes.STRING,
|
||||||
|
ownerEmail: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
preventIndexing: DataTypes.BOOLEAN
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'feed'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(Feed)
|
||||||
|
Feed.belongsTo(user)
|
||||||
|
|
||||||
|
libraryItem.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'libraryItem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
collection.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'collection'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
series.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'series'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
playlist.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'playlist'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
Feed.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
|
||||||
|
instance.entity = instance.libraryItem
|
||||||
|
instance.dataValues.entity = instance.dataValues.libraryItem
|
||||||
|
} else if (instance.entityType === 'collection' && instance.collection !== undefined) {
|
||||||
|
instance.entity = instance.collection
|
||||||
|
instance.dataValues.entity = instance.dataValues.collection
|
||||||
|
} else if (instance.entityType === 'series' && instance.series !== undefined) {
|
||||||
|
instance.entity = instance.series
|
||||||
|
instance.dataValues.entity = instance.dataValues.series
|
||||||
|
} else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
|
||||||
|
instance.entity = instance.playlist
|
||||||
|
instance.dataValues.entity = instance.dataValues.playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.libraryItem
|
||||||
|
delete instance.dataValues.libraryItem
|
||||||
|
delete instance.collection
|
||||||
|
delete instance.dataValues.collection
|
||||||
|
delete instance.series
|
||||||
|
delete instance.dataValues.series
|
||||||
|
delete instance.playlist
|
||||||
|
delete instance.dataValues.playlist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Feed
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class FeedEpisode extends Model { }
|
||||||
|
|
||||||
|
FeedEpisode.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
siteURL: DataTypes.STRING,
|
||||||
|
enclosureURL: DataTypes.STRING,
|
||||||
|
enclosureType: DataTypes.STRING,
|
||||||
|
enclosureSize: DataTypes.BIGINT,
|
||||||
|
pubDate: DataTypes.STRING,
|
||||||
|
season: DataTypes.STRING,
|
||||||
|
episode: DataTypes.STRING,
|
||||||
|
episodeType: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
filePath: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'feedEpisode'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { feed } = sequelize.models
|
||||||
|
|
||||||
|
feed.hasMany(FeedEpisode)
|
||||||
|
FeedEpisode.belongsTo(feed)
|
||||||
|
|
||||||
|
return FeedEpisode
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class FileMetadata extends Model { }
|
||||||
|
|
||||||
|
FileMetadata.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
ino: DataTypes.STRING,
|
||||||
|
filename: DataTypes.STRING,
|
||||||
|
ext: DataTypes.STRING,
|
||||||
|
path: DataTypes.STRING,
|
||||||
|
size: DataTypes.BIGINT,
|
||||||
|
mtime: DataTypes.DATE(6),
|
||||||
|
ctime: DataTypes.DATE(6),
|
||||||
|
birthtime: DataTypes.DATE(6)
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
freezeTableName: true, // sequelize uses datum as singular of data
|
||||||
|
name: {
|
||||||
|
singular: 'fileMetadata',
|
||||||
|
plural: 'fileMetadata'
|
||||||
|
},
|
||||||
|
modelName: 'fileMetadata'
|
||||||
|
})
|
||||||
|
|
||||||
|
return FileMetadata
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Genre extends Model { }
|
||||||
|
|
||||||
|
Genre.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
cleanName: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'genre'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Genre
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Library extends Model { }
|
||||||
|
|
||||||
|
Library.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
displayOrder: DataTypes.INTEGER,
|
||||||
|
icon: DataTypes.STRING,
|
||||||
|
mediaType: DataTypes.STRING,
|
||||||
|
provider: DataTypes.STRING,
|
||||||
|
lastScan: DataTypes.DATE,
|
||||||
|
lastScanVersion: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'library'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Library
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibraryFile extends Model { }
|
||||||
|
|
||||||
|
LibraryFile.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'libraryFile'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { libraryItem, fileMetadata } = sequelize.models
|
||||||
|
libraryItem.hasMany(LibraryFile)
|
||||||
|
LibraryFile.belongsTo(libraryItem)
|
||||||
|
|
||||||
|
fileMetadata.hasOne(LibraryFile, { foreignKey: 'fileMetadataId' })
|
||||||
|
LibraryFile.belongsTo(fileMetadata, { as: 'fileMetadata', foreignKey: 'fileMetadataId' })
|
||||||
|
|
||||||
|
return LibraryFile
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibraryFolder extends Model { }
|
||||||
|
|
||||||
|
LibraryFolder.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
path: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'libraryFolder'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library } = sequelize.models
|
||||||
|
library.hasMany(LibraryFolder)
|
||||||
|
LibraryFolder.belongsTo(library)
|
||||||
|
|
||||||
|
return LibraryFolder
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibraryItem extends Model {
|
||||||
|
getMedia(options) {
|
||||||
|
if (!this.mediaType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryItem.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
ino: DataTypes.STRING,
|
||||||
|
path: DataTypes.STRING,
|
||||||
|
relPath: DataTypes.STRING,
|
||||||
|
mediaId: DataTypes.UUIDV4,
|
||||||
|
mediaType: DataTypes.STRING,
|
||||||
|
isFile: DataTypes.BOOLEAN,
|
||||||
|
isMissing: DataTypes.BOOLEAN,
|
||||||
|
isInvalid: DataTypes.BOOLEAN,
|
||||||
|
mtime: DataTypes.DATE(6),
|
||||||
|
ctime: DataTypes.DATE(6),
|
||||||
|
birthtime: DataTypes.DATE(6),
|
||||||
|
lastScan: DataTypes.DATE,
|
||||||
|
lastScanVersion: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'libraryItem'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library, libraryFolder, book, podcast } = sequelize.models
|
||||||
|
library.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(library)
|
||||||
|
|
||||||
|
libraryFolder.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(libraryFolder)
|
||||||
|
|
||||||
|
book.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
podcast.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'podcast'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
LibraryItem.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.media = instance.book
|
||||||
|
instance.dataValues.media = instance.dataValues.book
|
||||||
|
} else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) {
|
||||||
|
instance.media = instance.podcast
|
||||||
|
instance.dataValues.media = instance.dataValues.podcast
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcast
|
||||||
|
delete instance.dataValues.podcast
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return LibraryItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibrarySetting extends Model { }
|
||||||
|
|
||||||
|
LibrarySetting.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
key: DataTypes.STRING,
|
||||||
|
value: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'librarySetting'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library } = sequelize.models
|
||||||
|
|
||||||
|
library.hasMany(LibrarySetting)
|
||||||
|
LibrarySetting.belongsTo(library)
|
||||||
|
|
||||||
|
return LibrarySetting
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class MediaFile extends Model { }
|
||||||
|
|
||||||
|
MediaFile.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
formatName: DataTypes.STRING,
|
||||||
|
formatNameLong: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
bitrate: DataTypes.INTEGER,
|
||||||
|
size: DataTypes.BIGINT,
|
||||||
|
tags: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'mediaFile'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fileMetadata } = sequelize.models
|
||||||
|
|
||||||
|
fileMetadata.hasOne(MediaFile, { foreignKey: 'fileMetadataId' })
|
||||||
|
MediaFile.belongsTo(fileMetadata, { as: 'fileMetadata', foreignKey: 'fileMetadataId' })
|
||||||
|
|
||||||
|
return MediaFile
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Book has many MediaProgress. PodcastEpisode has many MediaProgress.
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class MediaProgress extends Model {
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaProgress.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
currentTime: DataTypes.FLOAT,
|
||||||
|
isFinished: DataTypes.BOOLEAN,
|
||||||
|
hideFromContinueListening: DataTypes.BOOLEAN,
|
||||||
|
finishedAt: DataTypes.DATE
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'mediaProgress'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, user } = sequelize.models
|
||||||
|
|
||||||
|
book.hasMany(MediaProgress, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasMany(MediaProgress, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
MediaProgress.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user.hasMany(MediaProgress)
|
||||||
|
MediaProgress.belongsTo(user)
|
||||||
|
|
||||||
|
return MediaProgress
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class MediaStream extends Model { }
|
||||||
|
|
||||||
|
MediaStream.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
index: DataTypes.INTEGER,
|
||||||
|
codecType: DataTypes.STRING,
|
||||||
|
codec: DataTypes.STRING,
|
||||||
|
channels: DataTypes.INTEGER,
|
||||||
|
channelLayout: DataTypes.STRING,
|
||||||
|
bitrate: DataTypes.INTEGER,
|
||||||
|
timeBase: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
sampleRate: DataTypes.INTEGER,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
default: DataTypes.BOOLEAN,
|
||||||
|
// Video stream specific
|
||||||
|
profile: DataTypes.STRING,
|
||||||
|
width: DataTypes.INTEGER,
|
||||||
|
height: DataTypes.INTEGER,
|
||||||
|
codedWidth: DataTypes.INTEGER,
|
||||||
|
codedHeight: DataTypes.INTEGER,
|
||||||
|
pixFmt: DataTypes.STRING,
|
||||||
|
level: DataTypes.INTEGER,
|
||||||
|
frameRate: DataTypes.FLOAT,
|
||||||
|
colorSpace: DataTypes.STRING,
|
||||||
|
colorRange: DataTypes.STRING,
|
||||||
|
chromaLocation: DataTypes.STRING,
|
||||||
|
displayAspectRatio: DataTypes.FLOAT,
|
||||||
|
// Chapters JSON
|
||||||
|
chapters: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'mediaStream'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mediaFile } = sequelize.models
|
||||||
|
|
||||||
|
mediaFile.hasMany(MediaStream)
|
||||||
|
MediaStream.belongsTo(mediaFile)
|
||||||
|
|
||||||
|
return MediaStream
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Notification extends Model { }
|
||||||
|
|
||||||
|
Notification.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
eventName: DataTypes.STRING,
|
||||||
|
urls: DataTypes.JSON, // JSON array of urls
|
||||||
|
titleTemplate: DataTypes.STRING(1000),
|
||||||
|
bodyTemplate: DataTypes.TEXT,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
lastFiredAt: DataTypes.DATE,
|
||||||
|
lastAttemptFailed: DataTypes.BOOLEAN,
|
||||||
|
numConsecutiveFailedAttempts: DataTypes.INTEGER,
|
||||||
|
numTimesFired: DataTypes.INTEGER,
|
||||||
|
enabled: DataTypes.BOOLEAN,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'notification'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Notification
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Person extends Model { }
|
||||||
|
|
||||||
|
Person.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
asin: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'person'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fileMetadata } = sequelize.models
|
||||||
|
fileMetadata.hasMany(Person, { foreignKey: 'imageFileId' })
|
||||||
|
Person.belongsTo(fileMetadata, { as: 'imageFile', foreignKey: 'imageFileId' }) // Ref: https://sequelize.org/docs/v6/core-concepts/assocs/#defining-an-alias
|
||||||
|
|
||||||
|
return Person
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PlaybackSession extends Model {
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackSession.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
displayTitle: DataTypes.STRING,
|
||||||
|
displayAuthor: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
playMethod: DataTypes.STRING,
|
||||||
|
mediaPlayer: DataTypes.STRING,
|
||||||
|
startTime: DataTypes.FLOAT,
|
||||||
|
currentTime: DataTypes.FLOAT,
|
||||||
|
serverVersion: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'playbackSession'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, user, device } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(PlaybackSession)
|
||||||
|
PlaybackSession.belongsTo(user)
|
||||||
|
|
||||||
|
device.hasMany(PlaybackSession)
|
||||||
|
PlaybackSession.belongsTo(device)
|
||||||
|
|
||||||
|
book.hasMany(PlaybackSession, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasOne(PlaybackSession, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
PlaybackSession.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return PlaybackSession
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PlaybackSessionListenTime extends Model { }
|
||||||
|
|
||||||
|
PlaybackSessionListenTime.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
time: DataTypes.INTEGER,
|
||||||
|
date: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'playbackSessionListenTime'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { playbackSession } = sequelize.models
|
||||||
|
|
||||||
|
playbackSession.hasMany(PlaybackSessionListenTime)
|
||||||
|
PlaybackSessionListenTime.belongsTo(playbackSession)
|
||||||
|
|
||||||
|
return PlaybackSessionListenTime
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Playlist extends Model { }
|
||||||
|
|
||||||
|
Playlist.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'playlist'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library, user } = sequelize.models
|
||||||
|
library.hasMany(Playlist)
|
||||||
|
Playlist.belongsTo(library)
|
||||||
|
|
||||||
|
user.hasMany(Playlist)
|
||||||
|
Playlist.belongsTo(user)
|
||||||
|
|
||||||
|
return Playlist
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PlaylistMediaItem extends Model {
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistMediaItem.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
timestamps: true,
|
||||||
|
updatedAt: false,
|
||||||
|
modelName: 'playlistMediaItem'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, playlist } = sequelize.models
|
||||||
|
|
||||||
|
book.hasMany(PlaylistMediaItem, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasOne(PlaylistMediaItem, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
PlaylistMediaItem.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
playlist.hasMany(PlaylistMediaItem)
|
||||||
|
PlaylistMediaItem.belongsTo(playlist)
|
||||||
|
|
||||||
|
return PlaylistMediaItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Podcast extends Model { }
|
||||||
|
|
||||||
|
Podcast.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
releaseDate: DataTypes.STRING,
|
||||||
|
feedURL: DataTypes.STRING,
|
||||||
|
imageURL: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
itunesPageURL: DataTypes.STRING,
|
||||||
|
itunesId: DataTypes.STRING,
|
||||||
|
itunesArtistId: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
podcastType: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
|
autoDownloadEpisodes: DataTypes.BOOLEAN,
|
||||||
|
autoDownloadSchedule: DataTypes.STRING,
|
||||||
|
lastEpisodeCheck: DataTypes.DATE,
|
||||||
|
maxEpisodesToKeep: DataTypes.INTEGER,
|
||||||
|
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||||
|
lastCoverSearchQuery: DataTypes.STRING,
|
||||||
|
lastCoverSearch: DataTypes.DATE
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcast'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fileMetadata } = sequelize.models
|
||||||
|
|
||||||
|
fileMetadata.hasOne(Podcast, { foreignKey: 'imageFileId' })
|
||||||
|
Podcast.belongsTo(fileMetadata, { as: 'imageFile', foreignKey: 'imageFileId' }) // Ref: https://sequelize.org/docs/v6/core-concepts/assocs/#defining-an-alias
|
||||||
|
|
||||||
|
return Podcast
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PodcastEpisode extends Model { }
|
||||||
|
|
||||||
|
PodcastEpisode.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
index: DataTypes.INTEGER,
|
||||||
|
season: DataTypes.STRING,
|
||||||
|
episode: DataTypes.STRING,
|
||||||
|
episodeType: DataTypes.STRING,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
subtitle: DataTypes.STRING(1000),
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
pubDate: DataTypes.STRING,
|
||||||
|
enclosureURL: DataTypes.STRING,
|
||||||
|
enclosureSize: DataTypes.BIGINT,
|
||||||
|
enclosureType: DataTypes.STRING,
|
||||||
|
publishedAt: DataTypes.DATE
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcastEpisode'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { podcast } = sequelize.models
|
||||||
|
podcast.hasMany(PodcastEpisode)
|
||||||
|
PodcastEpisode.belongsTo(podcast)
|
||||||
|
|
||||||
|
return PodcastEpisode
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PodcastGenre extends Model { }
|
||||||
|
|
||||||
|
PodcastGenre.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcastGenre',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { podcast, genre } = sequelize.models
|
||||||
|
podcast.belongsToMany(genre, { through: PodcastGenre })
|
||||||
|
genre.belongsToMany(podcast, { through: PodcastGenre })
|
||||||
|
|
||||||
|
podcast.hasMany(PodcastGenre)
|
||||||
|
PodcastGenre.belongsTo(podcast)
|
||||||
|
|
||||||
|
genre.hasMany(PodcastGenre)
|
||||||
|
PodcastGenre.belongsTo(genre)
|
||||||
|
|
||||||
|
return PodcastGenre
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PodcastTag extends Model { }
|
||||||
|
|
||||||
|
PodcastTag.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcastTag',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { podcast, tag } = sequelize.models
|
||||||
|
podcast.belongsToMany(tag, { through: PodcastTag })
|
||||||
|
tag.belongsToMany(podcast, { through: PodcastTag })
|
||||||
|
|
||||||
|
podcast.hasMany(PodcastTag)
|
||||||
|
PodcastTag.belongsTo(podcast)
|
||||||
|
|
||||||
|
tag.hasMany(PodcastTag)
|
||||||
|
PodcastTag.belongsTo(tag)
|
||||||
|
|
||||||
|
return PodcastTag
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Series extends Model { }
|
||||||
|
|
||||||
|
Series.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'series'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Series
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Setting extends Model { }
|
||||||
|
|
||||||
|
Setting.init({
|
||||||
|
key: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
value: DataTypes.STRING,
|
||||||
|
type: DataTypes.INTEGER
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'setting'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Setting
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Tag extends Model { }
|
||||||
|
|
||||||
|
Tag.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
cleanName: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'tag'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Tag
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class User extends Model { }
|
||||||
|
|
||||||
|
User.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
username: DataTypes.STRING,
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
pash: DataTypes.STRING,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
token: DataTypes.STRING,
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
isLocked: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
lastSeen: DataTypes.DATE,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
return User
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class UserPermission extends Model { }
|
||||||
|
|
||||||
|
UserPermission.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
key: DataTypes.STRING,
|
||||||
|
value: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'userPermission'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(UserPermission)
|
||||||
|
UserPermission.belongsTo(user)
|
||||||
|
|
||||||
|
return UserPermission
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const items = require('./items')
|
||||||
|
const libraries = require('./libraries')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use('/items', items)
|
||||||
|
router.use('/libraries', libraries)
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const LibraryItemController = require('../controllers2/item.controller')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.get('/:id', LibraryItemController.getLibraryItem)
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const LibraryController = require('../controllers2/library.controller')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.get('/', LibraryController.getAllLibraries)
|
||||||
|
router.get('/:id', LibraryController.getLibrary)
|
||||||
|
router.get('/:id/items', LibraryController.getLibraryItems)
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,33 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../../libs/fsExtra')
|
||||||
const njodb = require('../libs/njodb')
|
const njodb = require('../../libs/njodb')
|
||||||
|
|
||||||
const { SupportedEbookTypes } = require('./globals')
|
const { SupportedEbookTypes } = require('../globals')
|
||||||
const { PlayMethod } = require('./constants')
|
const { PlayMethod } = require('../constants')
|
||||||
const { getId } = require('./index')
|
const { getId } = require('../index')
|
||||||
const { filePathToPOSIX } = require('./fileUtils')
|
const { filePathToPOSIX } = require('../fileUtils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
const Library = require('../objects/Library')
|
const Library = require('../../objects/Library')
|
||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../../objects/LibraryItem')
|
||||||
const Book = require('../objects/mediaTypes/Book')
|
const Book = require('../../objects/mediaTypes/Book')
|
||||||
|
|
||||||
const BookMetadata = require('../objects/metadata/BookMetadata')
|
const BookMetadata = require('../../objects/metadata/BookMetadata')
|
||||||
const FileMetadata = require('../objects/metadata/FileMetadata')
|
const FileMetadata = require('../../objects/metadata/FileMetadata')
|
||||||
|
|
||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../../objects/files/AudioFile')
|
||||||
const EBookFile = require('../objects/files/EBookFile')
|
const EBookFile = require('../../objects/files/EBookFile')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../../objects/files/LibraryFile')
|
||||||
const AudioMetaTags = require('../objects/metadata/AudioMetaTags')
|
const AudioMetaTags = require('../../objects/metadata/AudioMetaTags')
|
||||||
|
|
||||||
const Author = require('../objects/entities/Author')
|
const Author = require('../../objects/entities/Author')
|
||||||
const Series = require('../objects/entities/Series')
|
const Series = require('../../objects/entities/Series')
|
||||||
|
|
||||||
const MediaProgress = require('../objects/user/MediaProgress')
|
const MediaProgress = require('../../objects/user/MediaProgress')
|
||||||
const PlaybackSession = require('../objects/PlaybackSession')
|
const PlaybackSession = require('../../objects/PlaybackSession')
|
||||||
|
|
||||||
const { isObject } = require('.')
|
const { isObject } = require('..')
|
||||||
const User = require('../objects/user/User')
|
const User = require('../../objects/user/User')
|
||||||
|
|
||||||
var authorsToAdd = []
|
var authorsToAdd = []
|
||||||
var existingDbAuthors = []
|
var existingDbAuthors = []
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const { once } = require('events')
|
||||||
|
const { createInterface } = require('readline')
|
||||||
|
const Path = require('path')
|
||||||
|
const Logger = require('../../Logger')
|
||||||
|
const fs = require('../../libs/fsExtra')
|
||||||
|
|
||||||
|
async function processDbFile(filepath) {
|
||||||
|
if (!fs.pathExistsSync(filepath)) {
|
||||||
|
Logger.error(`[oldDbFiles] Db file does not exist at "${filepath}"`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileStream = fs.createReadStream(filepath)
|
||||||
|
|
||||||
|
const rl = createInterface({
|
||||||
|
input: fileStream,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
})
|
||||||
|
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
if (line && line.trim()) {
|
||||||
|
try {
|
||||||
|
const entity = JSON.parse(line)
|
||||||
|
if (entity && Object.keys(entity).length) entities.push(entity)
|
||||||
|
} catch (jsonParseError) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to parse line "${line}" in db file "${filepath}"`, jsonParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await once(rl, 'close')
|
||||||
|
|
||||||
|
console.log(`[oldDbFiles] Db file "${filepath}" processed`)
|
||||||
|
|
||||||
|
return entities
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to read db file "${filepath}"`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDbData(dbpath) {
|
||||||
|
try {
|
||||||
|
Logger.info(`[oldDbFiles] Loading db data at "${dbpath}"`)
|
||||||
|
const files = await fs.readdir(dbpath)
|
||||||
|
|
||||||
|
const entities = []
|
||||||
|
for (const filename of files) {
|
||||||
|
if (Path.extname(filename).toLowerCase() !== '.json') {
|
||||||
|
Logger.warn(`[oldDbFiles] Ignoring filename "${filename}" in db folder "${dbpath}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = Path.join(dbpath, filename)
|
||||||
|
Logger.info(`[oldDbFiles] Loading db data file "${filepath}"`)
|
||||||
|
const someEntities = await processDbFile(filepath)
|
||||||
|
Logger.info(`[oldDbFiles] Processed db data file with ${someEntities.length} entities`)
|
||||||
|
entities.push(...someEntities)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[oldDbFiles] Finished loading db data with ${entities.length} entities`)
|
||||||
|
return entities
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to load db data "${dbpath}"`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.init = async () => {
|
||||||
|
const dbs = {
|
||||||
|
libraryItems: Path.join(global.ConfigPath, 'libraryItems', 'data'),
|
||||||
|
users: Path.join(global.ConfigPath, 'users', 'data'),
|
||||||
|
sessions: Path.join(global.ConfigPath, 'sessions', 'data'),
|
||||||
|
libraries: Path.join(global.ConfigPath, 'libraries', 'data'),
|
||||||
|
settings: Path.join(global.ConfigPath, 'settings', 'data'),
|
||||||
|
collections: Path.join(global.ConfigPath, 'collections', 'data'),
|
||||||
|
playlists: Path.join(global.ConfigPath, 'playlists', 'data'),
|
||||||
|
authors: Path.join(global.ConfigPath, 'authors', 'data'),
|
||||||
|
series: Path.join(global.ConfigPath, 'series', 'data'),
|
||||||
|
feeds: Path.join(global.ConfigPath, 'feeds', 'data')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {}
|
||||||
|
for (const key in dbs) {
|
||||||
|
data[key] = await loadDbData(dbs[key])
|
||||||
|
Logger.info(`[oldDbFiles] ${data[key].length} ${key} loaded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user