diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index 87080312ae..9a0ee4e045 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -208,6 +208,20 @@ public async Task>>> GetPagesReadPerYear return Ok(_statService.GetPagesReadCountByYear(userId)); } + /// + /// Returns a count of time spent reading per year for a given userId. + /// + /// If userId is 0 and user is not an admin, API will default to userId + /// + [HttpGet("time-spent-reading-per-year")] + [ResponseCache(CacheProfileName = "Statistics")] + public async Task>>> TimeSpentReadingPerYear(int userId = 0) + { + var isAdmin = User.IsInRole(PolicyConstants.AdminRole); + if (!isAdmin) userId = User.GetUserId(); + return Ok(_statService.GetTimeSpentReadingByYear(userId)); + } + /// /// Returns a count of words read per year for a given userId. /// diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index b2c5cbaebc..dafc5f7ea5 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -34,6 +34,7 @@ public interface IStatisticService IEnumerable> GetDayBreakdown(int userId = 0); IEnumerable> GetPagesReadCountByYear(int userId = 0); IEnumerable> GetWordsReadCountByYear(int userId = 0); + IEnumerable> GetTimeSpentReadingByYear(int userId = 0); Task UpdateServerStatistics(); Task TimeSpentReadingForUsersAsync(IList userIds, IList libraryIds); Task GetKavitaPlusMetadataBreakdown(); @@ -539,6 +540,27 @@ public async Task TimeSpentReadingForUsersAsync(IList userIds, IList< p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages)))); } +public IEnumerable> GetTimeSpentReadingByYear(int userId = 0) +{ + var query = _context.AppUserProgresses + .Join(_context.Chapter, + progress => progress.ChapterId, + chapter => chapter.Id, + (progress, chapter) => new { Progress = progress, Chapter = chapter }) + .AsSplitQuery() + .AsNoTracking(); + + if (userId > 0) + { + query = query.Where(p => p.Progress.AppUserId == userId); + } + + return query.GroupBy(p => p.Progress.LastModified.Year) + .OrderBy(g => g.Key) + .Select(g => new StatCount { Value = g.Key, Count = g.Sum(x => x.Chapter.AvgHoursToRead) }) + .AsEnumerable(); +} + public async Task GetKavitaPlusMetadataBreakdown() { // We need to count number of Series that have an external series record diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index f729084c57..9b33337426 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -83,6 +83,13 @@ export class StatisticsService { }))); } + getTimeSpentReadingPerYear(userId = 0) { + return this.httpClient.get[]>(this.baseUrl + 'stats/time-spent-reading-per-year?userId=' + userId).pipe( + map(spreads => spreads.map(spread => { + return {name: spread.value + '', value: spread.count}; + }))); + } + getTopUsers(days: number = 0) { return this.httpClient.get(this.baseUrl + 'stats/server/top/users?days=' + days); } diff --git a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html index 8cd4659147..de10be6dcd 100644 --- a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html +++ b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html @@ -11,7 +11,7 @@ -
+
{{totalWordsRead | compactNumber}} @@ -20,10 +20,10 @@
- +
- + {{timeSpentReading | timeDuration}}
diff --git a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts index 065d7ab1b3..5253705a92 100644 --- a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts +++ b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.ts @@ -46,4 +46,12 @@ export class UserStatsInfoCardsComponent { ref.componentInstance.title = 'Words Read By Year'; }); } + openTimeSpentReadingByYearList() { + const numberPipe = new CompactNumberPipe(); + this.statsService.getTimeSpentReadingPerYear().subscribe(yearCounts => { + const ref = this.modalService.open(GenericListModalComponent, { scrollable: true }); + ref.componentInstance.items = yearCounts.map(t => `${t.name}: ${numberPipe.transform(t.value)} hours`); + ref.componentInstance.title = 'Time Spent Reading By Year'; + }); + } }