Compare commits

...

19 Commits

Author SHA1 Message Date
advplyr
b4dc1c1f03 Merge branch 'master' into sqlite 2023-03-22 12:18:02 -05:00
advplyr
633e83a4ab Update libraryItem model to include libraryId 2023-03-21 17:06:08 -05:00
advplyr
d745e6b656 Merge branch 'master' into sqlite 2023-03-20 16:03:07 -05:00
advplyr
b62e88c4ed Update routes/controllers/db structure 2023-03-20 15:52:33 -05:00
advplyr
258b9ec54e Update library item example route 2023-03-19 17:41:27 -05:00
advplyr
54ca58e610 Update model casing & associations 2023-03-19 15:19:22 -05:00
advplyr
2131a65299 Merge branch 'master' into sqlite 2023-03-19 09:21:00 -05:00
advplyr
243bc7b0d0 Complete migration file 2023-03-18 16:56:57 -05:00
advplyr
b8de041497 Update migration file to use bulkCreate 2023-03-17 18:04:39 -05:00
advplyr
8287822354 Merge branch 'master' into sqlite 2023-03-17 17:08:11 -05:00
advplyr
0f83a292f6 Migration test force re-create tables 2023-03-15 17:50:47 -05:00
advplyr
c738e35a8c Starting db migration file 2023-03-15 17:42:35 -05:00
advplyr
b2e1e24ca5 Merge branch 'master' into sqlite 2023-03-14 16:20:08 -05:00
advplyr
c7f457da3e Feed and Setting models 2023-03-13 17:13:31 -05:00
advplyr
bed3758268 PlaybackSession, Playlist, PlaylistMediaItem, Device data models 2023-03-12 17:55:12 -05:00
advplyr
a1a923df94 Merge branch 'master' into sqlite 2023-03-12 16:15:59 -05:00
advplyr
bbf324ea83 Adding more models 2023-03-12 14:51:45 -05:00
advplyr
adc4309951 Merge branch 'master' into sqlite 2023-03-11 11:54:34 -06:00
advplyr
b8ab72a141 Sequelize and sqlite init with test user model 2023-03-08 12:33:52 -06:00
55 changed files with 5838 additions and 43 deletions

View File

@@ -14,7 +14,10 @@ RUN apk update && \
apk add --no-cache --update \
curl \
tzdata \
ffmpeg
ffmpeg \
make \
python3 \
g++
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
@@ -23,6 +26,8 @@ COPY server server
RUN npm ci --only=production
RUN apk del make python3 g++
EXPOSE 80
HEALTHCHECK \
--interval=30s \

2378
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -35,10 +35,12 @@
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
"node-tone": "^1.0.1",
"sequelize": "^6.29.1",
"socket.io": "^4.5.4",
"sqlite3": "^5.1.4",
"xml2js": "^0.4.23"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
}

93
server/Database.js Normal file
View File

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

View File

@@ -8,7 +8,8 @@ const rateLimit = require('./libs/expressRateLimit')
const { version } = require('../package.json')
// Utils
const dbMigration = require('./utils/dbMigration')
const dbMigration2 = require('./utils/migrations/dbMigrationOld')
const dbMigration3 = require('./utils/migrations/dbMigration')
const filePerms = require('./utils/filePerms')
const fileUtils = require('./utils/fileUtils')
const Logger = require('./Logger')
@@ -17,8 +18,11 @@ const Auth = require('./Auth')
const Watcher = require('./Watcher')
const Scanner = require('./scanner/Scanner')
const Db = require('./Db')
const Database = require('./Database')
const SocketAuthority = require('./SocketAuthority')
const routes = require('./routes/index')
const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter')
const StaticRouter = require('./routers/StaticRouter')
@@ -82,6 +86,7 @@ class Server {
// Routers
this.apiRouter = new ApiRouter(this)
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
this.staticRouter = new StaticRouter(this.db)
@@ -99,13 +104,18 @@ class Server {
Logger.info('[Server] Init v' + version)
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
if (previousVersion) {
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
}
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
await dbMigration.migrate(this.db)
await dbMigration2.migrate(this.db)
} else {
await this.db.init()
}
@@ -162,6 +172,7 @@ class Server {
// Static folder
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('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)

View File

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

View File

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

353
server/db/item.db.js Normal file
View File

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

24
server/db/library.db.js Normal file
View File

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

View File

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

View File

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

38
server/models/Book.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

31
server/models/BookTag.js Normal file
View File

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

View File

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

View File

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

29
server/models/Device.js Normal file
View File

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

View File

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

117
server/models/Feed.js Normal file
View File

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

View File

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

View File

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

20
server/models/Genre.js Normal file
View File

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

25
server/models/Library.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

26
server/models/Person.js Normal file
View File

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

View File

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

View File

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

27
server/models/Playlist.js Normal file
View File

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

View File

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

43
server/models/Podcast.js Normal file
View File

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

View File

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

View File

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

View File

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

20
server/models/Series.js Normal file
View File

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

19
server/models/Setting.js Normal file
View File

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

20
server/models/Tag.js Normal file
View File

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

33
server/models/User.js Normal file
View File

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

View File

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

10
server/routes/index.js Normal file
View File

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

8
server/routes/items.js Normal file
View File

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

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,33 @@
const Path = require('path')
const fs = require('../libs/fsExtra')
const njodb = require('../libs/njodb')
const fs = require('../../libs/fsExtra')
const njodb = require('../../libs/njodb')
const { SupportedEbookTypes } = require('./globals')
const { PlayMethod } = require('./constants')
const { getId } = require('./index')
const { filePathToPOSIX } = require('./fileUtils')
const Logger = require('../Logger')
const { SupportedEbookTypes } = require('../globals')
const { PlayMethod } = require('../constants')
const { getId } = require('../index')
const { filePathToPOSIX } = require('../fileUtils')
const Logger = require('../../Logger')
const Library = require('../objects/Library')
const LibraryItem = require('../objects/LibraryItem')
const Book = require('../objects/mediaTypes/Book')
const Library = require('../../objects/Library')
const LibraryItem = require('../../objects/LibraryItem')
const Book = require('../../objects/mediaTypes/Book')
const BookMetadata = require('../objects/metadata/BookMetadata')
const FileMetadata = require('../objects/metadata/FileMetadata')
const BookMetadata = require('../../objects/metadata/BookMetadata')
const FileMetadata = require('../../objects/metadata/FileMetadata')
const AudioFile = require('../objects/files/AudioFile')
const EBookFile = require('../objects/files/EBookFile')
const LibraryFile = require('../objects/files/LibraryFile')
const AudioMetaTags = require('../objects/metadata/AudioMetaTags')
const AudioFile = require('../../objects/files/AudioFile')
const EBookFile = require('../../objects/files/EBookFile')
const LibraryFile = require('../../objects/files/LibraryFile')
const AudioMetaTags = require('../../objects/metadata/AudioMetaTags')
const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
const Author = require('../../objects/entities/Author')
const Series = require('../../objects/entities/Series')
const MediaProgress = require('../objects/user/MediaProgress')
const PlaybackSession = require('../objects/PlaybackSession')
const MediaProgress = require('../../objects/user/MediaProgress')
const PlaybackSession = require('../../objects/PlaybackSession')
const { isObject } = require('.')
const User = require('../objects/user/User')
const { isObject } = require('..')
const User = require('../../objects/user/User')
var authorsToAdd = []
var existingDbAuthors = []

View File

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