diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 95e7c378c..a68891501 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -16,6 +16,9 @@ {{ $strings.ButtonLatest }} + + {{ $strings.ButtonCalendar }} + {{ $strings.ButtonSeries }} @@ -65,7 +68,7 @@ - + {{ $formatNumber(numShowing) }} {{ entityName }} @@ -247,6 +250,9 @@ export default { isPodcastLibrary() { return this.currentLibraryMediaType === 'podcast' }, + isCalendarPage() { + return this.page === 'calendar' + }, isLibraryPage() { return this.page === '' }, @@ -268,6 +274,9 @@ export default { isPodcastLatestPage() { return this.$route.name === 'library-library-podcast-latest' }, + isCalendarPage() { + return this.$route.name === 'library-library-calendar' + }, isPodcastDownloadQueuePage() { return this.$route.name === 'library-library-podcast-download-queue' }, diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 5f3642011..12170cdae 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -22,6 +22,12 @@ + + + {{ $strings.ButtonCalendar }} + + + @@ -173,6 +179,9 @@ export default { isPodcastLatestPage() { return this.$route.name === 'library-library-podcast-latest' }, + isCalendarPage() { + return this.$route.name === 'library-library-calendar' + }, homePage() { return this.$route.name === 'library-library' }, diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 7083c7890..3616f94e6 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -140,6 +140,10 @@ {{ $strings.LabelExample }}: {{ timeExample }} + + updateSettingsKey('calendarFirstDayOfWeek', val)" /> + + @@ -271,6 +275,12 @@ export default { timeExample() { const date = new Date(2014, 2, 25, 17, 30, 0) return this.$formatJsTime(date, this.newServerSettings.timeFormat) + }, + firstDayOfWeekOptions() { + return this.$store.state.globals.firstDayOfWeekOptions.map((option) => ({ + text: this.$strings[option.labelKey] || option.text, + value: option.value + })) } }, methods: { @@ -354,6 +364,10 @@ export default { this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])] this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher + if (this.newServerSettings.calendarFirstDayOfWeek === undefined) { + this.newServerSettings.calendarFirstDayOfWeek = 0 + } + this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL }, diff --git a/client/pages/library/_library/calendar.vue b/client/pages/library/_library/calendar.vue new file mode 100644 index 000000000..164042ff8 --- /dev/null +++ b/client/pages/library/_library/calendar.vue @@ -0,0 +1,384 @@ + + + + + + + + + + {{ $strings.HeaderCalendar }} + + + + + chevron_left + + + + + + + + + chevron_right + + + + + + {{ $strings.ButtonToday }} + + + + + + + {{ $strings.HeaderCalendar }} + + + + chevron_left + + + + + + + + + chevron_right + + + + {{ $strings.ButtonToday }} + + + + + + + + + + + + {{ day }} + + + + + + + + + + {{ day.dayNumber }} + + + + + + + {{ episode.podcastTitle }} + + + + S{{ episode.season }}E{{ episode.episode }} + + {{ episode.title }} + + + + + + + + + + + + + + diff --git a/client/store/globals.js b/client/store/globals.js index 7b416196a..fcc0622eb 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -71,6 +71,18 @@ export const state = () => ({ value: 'HH:mm' } ], + firstDayOfWeekOptions: [ + { + text: 'Sunday', + value: 0, + labelKey: 'LabelCalendarFirstDayOfWeekSunday' + }, + { + text: 'Monday', + value: 1, + labelKey: 'LabelCalendarFirstDayOfWeekMonday' + } + ], podcastTypes: [ { text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' }, { text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' } diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6dba7adb0..389a16fe7 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -14,6 +14,7 @@ "ButtonBatchEditPopulateFromExisting": "Populate from existing", "ButtonBatchEditPopulateMapDetails": "Populate map details", "ButtonBrowseForFolder": "Browse for Folder", + "ButtonCalendar": "Calendar", "ButtonCancel": "Cancel", "ButtonCancelEncode": "Cancel Encode", "ButtonChangeRootPassword": "Change Root Password", @@ -106,6 +107,7 @@ "ButtonStats": "Stats", "ButtonSubmit": "Submit", "ButtonTest": "Test", + "ButtonToday": "Today", "ButtonUnlinkOpenId": "Unlink OpenID", "ButtonUpload": "Upload", "ButtonUploadBackup": "Upload Backup", @@ -127,6 +129,7 @@ "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", + "HeaderCalendar": "Calendar", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", "HeaderChooseAFolder": "Choose a Folder", @@ -274,6 +277,35 @@ "LabelBooks": "Books", "LabelButtonText": "Button Text", "LabelByAuthor": "by {0}", + "LabelCalendarApril": "April", + "LabelCalendarAugust": "August", + "LabelCalendarDecember": "December", + "LabelCalendarFebruary": "February", + "LabelCalendarFirstDayOfWeek": "First day of week", + "LabelCalendarFirstDayOfWeekMonday": "Monday", + "LabelCalendarFirstDayOfWeekSunday": "Sunday", + "LabelCalendarFriday": "Friday", + "LabelCalendarFridayShort": "Fri", + "LabelCalendarJanuary": "January", + "LabelCalendarJuly": "July", + "LabelCalendarJune": "June", + "LabelCalendarMarch": "March", + "LabelCalendarMay": "May", + "LabelCalendarMonday": "Monday", + "LabelCalendarMondayShort": "Mon", + "LabelCalendarNovember": "November", + "LabelCalendarOctober": "October", + "LabelCalendarSaturday": "Saturday", + "LabelCalendarSaturdayShort": "Sat", + "LabelCalendarSeptember": "September", + "LabelCalendarSunday": "Sunday", + "LabelCalendarSundayShort": "Sun", + "LabelCalendarThursday": "Thursday", + "LabelCalendarThursdayShort": "Thu", + "LabelCalendarTuesday": "Tuesday", + "LabelCalendarTuesdayShort": "Tue", + "LabelCalendarWednesday": "Wednesday", + "LabelCalendarWednesdayShort": "Wed", "LabelChangePassword": "Change Password", "LabelChannels": "Channels", "LabelChapterCount": "{0} Chapters", diff --git a/server/Server.js b/server/Server.js index 1a8db4063..8ed90cd63 100644 --- a/server/Server.js +++ b/server/Server.js @@ -385,6 +385,7 @@ class Server { '/library/:library/series/:id?', '/library/:library/podcast/search', '/library/:library/podcast/latest', + '/library/:library/podcast/calendar', '/library/:library/podcast/download-queue', '/config/users/:id', '/config/users/:id/sessions', diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index e63441f0b..ac720c6de 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -1460,6 +1460,86 @@ class LibraryController { } } + /** + * GET: /api/libraries/:id/episodes-calendar + * Get podcast episodes for calendar view within date range + * + * @param {LibraryControllerRequest} req + * @param {Response} res + */ + async getEpisodesCalendar(req, res) { + const { start, end, limit = 1000 } = req.query + + if (!start || !end) { + return res.status(400).send('Start and end date parameters are required') + } + + const startDate = new Date(start) + const endDate = new Date(end) + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return res.status(400).send('Invalid date format') + } + + if (req.library.mediaType !== 'podcast') { + return res.status(400).send('Library is not a podcast library') + } + + const libraryItems = await Database.libraryItemModel.findAll({ + where: { libraryId: req.library.id }, + include: [ + { + model: Database.podcastModel, + include: [ + { + model: Database.podcastEpisodeModel, + where: { + publishedAt: { + [Sequelize.Op.between]: [startDate, endDate] + } + }, + required: true + } + ], + required: true + } + ] + }) + + const episodes = [] + for (const libraryItem of libraryItems) { + for (const episode of libraryItem.media.podcastEpisodes) { + episodes.push({ + id: episode.id, + title: episode.title, + subtitle: episode.subtitle, + description: episode.description, + season: episode.season, + episode: episode.episode, + episodeType: episode.episodeType, + publishedAt: episode.publishedAt, + duration: episode.audioFile?.duration || 0, + audioFile: episode.audioFile, + libraryItemId: libraryItem.id, + podcastId: libraryItem.media.id, + podcastTitle: libraryItem.media.title, + podcastAuthor: libraryItem.media.author, + podcastExplicit: libraryItem.media.explicit, + coverPath: libraryItem.media.coverPath + }) + } + } + + episodes.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt)) + + const limitedEpisodes = episodes.slice(0, parseInt(limit)) + + res.json({ + episodes: limitedEpisodes, + total: episodes.length + }) + } + /** * * @param {RequestWithUser} req diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 4f0aa97bc..e646144c7 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -53,6 +53,7 @@ class ServerSettings { this.dateFormat = 'MM/dd/yyyy' this.timeFormat = 'HH:mm' this.language = 'en-us' + this.calendarFirstDayOfWeek = 0 this.logLevel = Logger.logLevel @@ -120,6 +121,7 @@ class ServerSettings { this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.timeFormat = settings.timeFormat || 'HH:mm' this.language = settings.language || 'en-us' + this.calendarFirstDayOfWeek = settings.calendarFirstDayOfWeek !== undefined ? Number(settings.calendarFirstDayOfWeek) : 0 this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 @@ -231,6 +233,7 @@ class ServerSettings { dateFormat: this.dateFormat, timeFormat: this.timeFormat, language: this.language, + calendarFirstDayOfWeek: this.calendarFirstDayOfWeek, logLevel: this.logLevel, version: this.version, buildNumber: this.buildNumber, diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 6446ecc80..32a41ad36 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -75,6 +75,7 @@ class ApiRouter { this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) + this.router.get('/libraries/:id/episodes-calendar', LibraryController.middleware, LibraryController.getEpisodesCalendar.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
{{ $strings.ButtonLatest }}
{{ $strings.ButtonCalendar }}
{{ $strings.ButtonSeries }}
{{ $formatNumber(numShowing) }} {{ entityName }}
{{ $strings.LabelExample }}: {{ timeExample }}
{{ $strings.HeaderCalendar }}