Reading History (#1699)
* Added new stat graph for pages read over time for all users. * Switched to reading events rather than pages read to get a better scale * Changed query to use Created date as LastModified wont work since I just did a migration on all rows. * Small cleanup on graph * Read by day completed and ready for user stats page. * Changed the initial stat report to be in 1 day, to avoid people trying and ditching the software from muddying up the stats. * Cleaned up stats page such that stats around series show their image and tweaked some layout and wordings * Fixed recently read order * Put read history on user profile * Final cleanup, Robbie needs to do a CSS pass before release.
This commit is contained in:
parent
e43ead44da
commit
1c1e48d28c
24 changed files with 426 additions and 86 deletions
|
@ -79,7 +79,7 @@ public class StatsController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns
|
||||
/// Returns users with the top reads in the server
|
||||
/// </summary>
|
||||
/// <param name="days"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -91,6 +91,10 @@ public class StatsController : BaseApiController
|
|||
return Ok(await _statService.GetTopUsers(days));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A breakdown of different files, their size, and format
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpGet("server/file-breakdown")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
|
@ -100,11 +104,30 @@ public class StatsController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns reading history events for a give or all users, broken up by day, and format
|
||||
/// </summary>
|
||||
/// <param name="userId">If 0, defaults to all users, else just userId</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("reading-count-by-day")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public async Task<ActionResult<IEnumerable<PagesReadOnADayCount<DateTime>>>> ReadCountByDay(int userId = 0)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
if (!isAdmin && userId != user.Id) return BadRequest();
|
||||
|
||||
return Ok(await _statService.ReadCountByDay(userId));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("user/reading-history")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public async Task<ActionResult<IEnumerable<ReadHistoryEvent>>> GetReadingHistory(int userId)
|
||||
{
|
||||
// TODO: Put a check in if the calling user is said userId or has admin
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
if (!isAdmin && userId != user.Id) return BadRequest();
|
||||
|
||||
return Ok(await _statService.GetReadingHistory(userId));
|
||||
}
|
||||
|
|
|
@ -55,6 +55,13 @@ public class UsersController : BaseApiController
|
|||
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("myself")]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetMyself()
|
||||
{
|
||||
var users = await _unitOfWork.UserRepository.GetAllUsersAsync();
|
||||
return Ok(users.Where(u => u.UserName == User.GetUsername()).DefaultIfEmpty().Select(u => _mapper.Map<MemberDto>(u)).SingleOrDefault());
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("has-reading-progress")]
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
|
|
21
API/DTOs/Statistics/PagesReadOnADayCount.cs
Normal file
21
API/DTOs/Statistics/PagesReadOnADayCount.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Statistics;
|
||||
|
||||
public class PagesReadOnADayCount<T> : ICount<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The day of the readings
|
||||
/// </summary>
|
||||
public T Value { get; set; }
|
||||
/// <summary>
|
||||
/// Number of pages read
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
/// <summary>
|
||||
/// Format of those files
|
||||
/// </summary>
|
||||
public MangaFormat Format { get; set; }
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ public interface IStatisticService
|
|||
Task<IEnumerable<TopReadDto>> GetTopUsers(int days);
|
||||
Task<IEnumerable<ReadHistoryEvent>> GetReadingHistory(int userId);
|
||||
Task<IEnumerable<ReadHistoryEvent>> GetHistory();
|
||||
Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -51,7 +52,6 @@ public class StatisticService : IStatisticService
|
|||
if (libraryIds.Count == 0)
|
||||
libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
|
||||
// Total Pages Read
|
||||
var totalPagesRead = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
|
@ -226,19 +226,20 @@ public class StatisticService : IStatisticService
|
|||
.OrderByDescending(d => d.Count)
|
||||
.Take(5);
|
||||
|
||||
var seriesIds = (await _context.AppUserProgresses
|
||||
.AsSplitQuery()
|
||||
.OrderByDescending(d => d.LastModified)
|
||||
.Select(d => d.SeriesId)
|
||||
.ToListAsync())
|
||||
.Distinct()
|
||||
// Remember: Ordering does not apply if there is a distinct
|
||||
var recentlyRead = _context.AppUserProgresses
|
||||
.Join(_context.Series, p => p.SeriesId, s => s.Id,
|
||||
(appUserProgresses, series) => new
|
||||
{
|
||||
Series = series,
|
||||
AppUserProgresses = appUserProgresses
|
||||
})
|
||||
.AsEnumerable()
|
||||
.DistinctBy(s => s.AppUserProgresses.SeriesId)
|
||||
.OrderByDescending(x => x.AppUserProgresses.LastModified)
|
||||
.Select(x => _mapper.Map<SeriesDto>(x.Series))
|
||||
.Take(5);
|
||||
|
||||
var recentlyRead = _context.Series
|
||||
.AsSplitQuery()
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
|
||||
var distinctPeople = _context.Person
|
||||
.AsSplitQuery()
|
||||
|
@ -281,6 +282,7 @@ public class StatisticService : IStatisticService
|
|||
TotalSize = _context.MangaFile.Where(mf2 => mf2.Extension == mf.Key).Distinct().Sum(mf2 => mf2.Bytes),
|
||||
TotalFiles = _context.MangaFile.Where(mf2 => mf2.Extension == mf.Key).Distinct().Count()
|
||||
})
|
||||
.OrderBy(d => d.TotalFiles)
|
||||
.ToListAsync(),
|
||||
TotalFileSize = await _context.MangaFile
|
||||
.AsNoTracking()
|
||||
|
@ -310,14 +312,36 @@ public class StatisticService : IStatisticService
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public void ReadCountByDay()
|
||||
public async Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0)
|
||||
{
|
||||
// _context.AppUserProgresses
|
||||
// .GroupBy(p => p.LastModified.Day)
|
||||
// .Select(g =>
|
||||
// {
|
||||
// Day = g.Key,
|
||||
// })
|
||||
var query = _context.AppUserProgresses
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id,
|
||||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Join(_context.Volume, x => x.chapter.VolumeId, volume => volume.Id,
|
||||
(x, volume) => new {x.appUserProgresses, x.chapter, volume})
|
||||
.Join(_context.Series, x => x.appUserProgresses.SeriesId, series => series.Id,
|
||||
(x, series) => new {x.appUserProgresses, x.chapter, x.volume, series});
|
||||
|
||||
if (userId > 0)
|
||||
{
|
||||
query = query.Where(x => x.appUserProgresses.AppUserId == userId);
|
||||
}
|
||||
|
||||
return await query.GroupBy(x => new
|
||||
{
|
||||
Day = x.appUserProgresses.Created.Date,
|
||||
x.series.Format
|
||||
})
|
||||
.Select(g => new PagesReadOnADayCount<DateTime>
|
||||
{
|
||||
Value = g.Key.Day,
|
||||
Format = g.Key.Format,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderBy(d => d.Value)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<ReadHistoryEvent>> GetHistory()
|
||||
|
@ -329,12 +353,12 @@ public class StatisticService : IStatisticService
|
|||
// .Select(sm => new
|
||||
// {
|
||||
// User = _context.AppUser.Single(u => u.Id == sm.Key),
|
||||
// Chapters = _context.Chapter.Where(c => _context.AppUserProgresses
|
||||
// .Where(u => u.AppUserId == sm.Key)
|
||||
// .Where(p => p.PagesRead > 0)
|
||||
// .Select(p => p.ChapterId)
|
||||
// .Distinct()
|
||||
// .Contains(c.Id))
|
||||
// Chapters = _context.Chapter.Where(c => _context.AppUserProgresses
|
||||
// .Where(u => u.AppUserId == sm.Key)
|
||||
// .Where(p => p.PagesRead > 0)
|
||||
// .Select(p => p.ChapterId)
|
||||
// .Distinct()
|
||||
// .Contains(c.Id))
|
||||
// })
|
||||
// .OrderByDescending(d => d.Chapters.Sum(c => c.AvgHoursToRead))
|
||||
// .Take(5)
|
||||
|
|
|
@ -147,6 +147,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
/// <summary>
|
||||
/// First time run stat collection. Executes immediately on a background thread. Does not block.
|
||||
/// </summary>
|
||||
/// <remarks>Schedules it for 1 day in the future to ensure we don't have users that try the software out</remarks>
|
||||
public async Task RunStatCollection()
|
||||
{
|
||||
var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection;
|
||||
|
@ -155,7 +156,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
_logger.LogDebug("User has opted out of stat collection, not sending stats");
|
||||
return;
|
||||
}
|
||||
BackgroundJob.Enqueue(() => _statsService.Send());
|
||||
BackgroundJob.Schedule(() => _statsService.Send(), DateTimeOffset.Now.AddDays(1));
|
||||
}
|
||||
|
||||
public void ScanSiteThemes()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue