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,