diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 0b18457e..c9989868 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -213,11 +213,16 @@ export default { isAuthorsPage() { return this.$route.name === 'library-library-authors' }, + isAlbumsPage() { + return this.page === 'albums' + }, numShowing() { return this.totalEntities }, entityName() { + if (this.isAlbumsPage) return 'Albums' if (this.isMusicLibrary) return 'Tracks' + if (this.isPodcastLibrary) return this.$strings.LabelPodcasts if (!this.page) return this.$strings.LabelBooks if (this.isSeriesPage) return this.$strings.LabelSeries diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 5e5a25c6..58578fae 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -205,7 +205,7 @@ export default { return this.$store.state.globals.selectedMediaItems || [] }, sizeMultiplier() { - var baseSize = this.isCoverSquareAspectRatio ? 192 : 120 + const baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.entityWidth / baseSize } }, @@ -215,7 +215,7 @@ export default { }, editEntity(entity) { if (this.entityName === 'books' || this.entityName === 'series-books') { - var bookIds = this.entities.map((e) => e.id) + const bookIds = this.entities.map((e) => e.id) this.$store.commit('setBookshelfBookIds', bookIds) this.$store.commit('showEditModal', entity) } else if (this.entityName === 'collections') { @@ -308,7 +308,7 @@ export default { } }, async fetchEntites(page = 0) { - var startIndex = page * this.booksPerFetch + const startIndex = page * this.booksPerFetch this.isFetchingEntities = true diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index caa57259..174ec372 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -70,6 +70,14 @@
+ + album + +

Albums

+ +
+ + queue_music @@ -138,12 +146,18 @@ export default { isPodcastLibrary() { return this.currentLibraryMediaType === 'podcast' }, + isMusicLibrary() { + return this.currentLibraryMediaType === 'music' + }, isPodcastSearchPage() { return this.$route.name === 'library-library-podcast-search' }, isPodcastLatestPage() { return this.$route.name === 'library-library-podcast-latest' }, + isMusicAlbumsPage() { + return this.paramId === 'albums' + }, homePage() { return this.$route.name === 'library-library' }, diff --git a/client/components/cards/LazyAlbumCard.vue b/client/components/cards/LazyAlbumCard.vue new file mode 100644 index 00000000..aba07840 --- /dev/null +++ b/client/components/cards/LazyAlbumCard.vue @@ -0,0 +1,110 @@ + + + \ No newline at end of file diff --git a/client/components/covers/PreviewCover.vue b/client/components/covers/PreviewCover.vue index 2707132a..fac0271a 100644 --- a/client/components/covers/PreviewCover.vue +++ b/client/components/covers/PreviewCover.vue @@ -65,7 +65,8 @@ export default { return `${this.naturalWidth}x${this.naturalHeight}px` }, placeholderUrl() { - return `${this.$config.routerBasePath}/book_placeholder.jpg` + const config = this.$config || this.$nuxt.$config + return `${config.routerBasePath}/book_placeholder.jpg` } }, methods: { diff --git a/client/mixins/bookshelfCardsHelpers.js b/client/mixins/bookshelfCardsHelpers.js index 26f5a9df..2d968806 100644 --- a/client/mixins/bookshelfCardsHelpers.js +++ b/client/mixins/bookshelfCardsHelpers.js @@ -3,6 +3,7 @@ import LazyBookCard from '@/components/cards/LazyBookCard' import LazySeriesCard from '@/components/cards/LazySeriesCard' import LazyCollectionCard from '@/components/cards/LazyCollectionCard' import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard' +import LazyAlbumCard from '@/components/cards/LazyAlbumCard' export default { data() { @@ -17,6 +18,7 @@ export default { if (this.entityName === 'series') return Vue.extend(LazySeriesCard) if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard) if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard) + if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard) return Vue.extend(LazyBookCard) }, async mountEntityCard(index) { @@ -28,7 +30,7 @@ export default { } this.entityIndexesMounted.push(index) if (this.entityComponentRefs[index]) { - var bookComponent = this.entityComponentRefs[index] + const bookComponent = this.entityComponentRefs[index] shelfEl.appendChild(bookComponent.$el) if (this.isSelectionMode) { bookComponent.setSelectionMode(true) @@ -43,13 +45,13 @@ export default { bookComponent.isHovering = false return } - var shelfOffsetY = 16 - var row = index % this.entitiesPerShelf - var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft + const shelfOffsetY = 16 + const row = index % this.entitiesPerShelf + const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft - var ComponentClass = this.getComponentClass() + const ComponentClass = this.getComponentClass() - var props = { + const props = { index, width: this.entityWidth, height: this.entityHeight, @@ -65,8 +67,8 @@ export default { props.orderBy = this.seriesSortBy } - var _this = this - var instance = new ComponentClass({ + const _this = this + const instance = new ComponentClass({ propsData: props, created() { this.$on('edit', (entity) => { diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 2b0aac25..b99e534b 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -492,6 +492,32 @@ class LibraryController { res.json(payload) } + // api/libraries/:id/albums + async getAlbumsForLibrary(req, res) { + if (!req.library.isMusic) { + return res.status(400).send('Invalid library media type') + } + + let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id) + let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems) + albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title + + const payload = { + results: [], + total: albums.length, + limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, + page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0 + } + + if (payload.limit) { + const startIndex = payload.page * payload.limit + albums = albums.slice(startIndex, startIndex + payload.limit) + } + + payload.results = albums + res.json(payload) + } + async getLibraryFilterData(req, res) { res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems)) } diff --git a/server/objects/Library.js b/server/objects/Library.js index 3432c4ea..48106c68 100644 --- a/server/objects/Library.js +++ b/server/objects/Library.js @@ -29,6 +29,9 @@ class Library { get isPodcast() { return this.mediaType === 'podcast' } + get isMusic() { + return this.mediaType === 'music' + } construct(library) { this.id = library.id diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index fe3ec3f0..28b07547 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -76,6 +76,7 @@ class ApiRouter { this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) + this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this)) this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 7e93864b..1e480845 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -1,4 +1,5 @@ const { sort, createNewSortInstance } = require('../libs/fastSort') +const Logger = require('../Logger') const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare @@ -701,5 +702,34 @@ module.exports = { return shelf }) + }, + + groupMusicLibraryItemsIntoAlbums(libraryItems) { + const albums = {} + + libraryItems.forEach((li) => { + const albumTitle = li.media.metadata.album + const albumArtist = li.media.metadata.albumArtist + + if (albumTitle && !albums[albumTitle]) { + albums[albumTitle] = { + title: albumTitle, + artist: albumArtist, + libraryItemId: li.media.coverPath ? li.id : null, + numTracks: 1 + } + } else if (albumTitle && albums[albumTitle].artist === albumArtist) { + if (!albums[albumTitle].libraryItemId && li.media.coverPath) albums[albumTitle].libraryItemId = li.id + albums[albumTitle].numTracks++ + } else { + if (albumTitle) { + Logger.warn(`Music track "${li.media.metadata.title}" with album "${albumTitle}" has a different album artist then another track in the same album. This track album artist is "${albumArtist}" but the album artist is already set to "${albums[albumTitle].artist}"`) + } + if (!albums['_none_']) albums['_none_'] = { title: 'No Album', artist: 'Various Artists', libraryItemId: null, numTracks: 0 } + albums['_none_'].numTracks++ + } + }) + + return Object.values(albums) } } \ No newline at end of file diff --git a/server/utils/scandir.js b/server/utils/scandir.js index fbbc1a8a..bea54cce 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -361,12 +361,12 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin // Called from Scanner.js async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, isSingleMediaItem, serverSettings = {}) { libraryItemPath = libraryItemPath.replace(/\\/g, '/') - var folderFullPath = folder.fullPath.replace(/\\/g, '/') + const folderFullPath = folder.fullPath.replace(/\\/g, '/') - var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) - var libraryItemData = {} + const libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) + let libraryItemData = {} - var fileItems = [] + let fileItems = [] if (isSingleMediaItem) { // Single media item in root of folder fileItems = [ @@ -388,8 +388,8 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings, fileNames) } - var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path) - var libraryItem = { + const libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path) + const libraryItem = { ino: libraryItemDirStats.ino, mtimeMs: libraryItemDirStats.mtimeMs || 0, ctimeMs: libraryItemDirStats.ctimeMs || 0,