statistics

This commit is contained in:
Stefan Hardegger
2025-10-21 10:53:33 +02:00
parent 16983fd871
commit 924ae12b5b
14 changed files with 1522 additions and 14 deletions

View File

@@ -42,6 +42,132 @@ public class LibraryStatisticsController {
}
}
/**
* Get top tags statistics
*/
@GetMapping("/top-tags")
public ResponseEntity<?> getTopTagsStatistics(
@PathVariable String libraryId,
@RequestParam(defaultValue = "20") int limit) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getTopTagsStatistics(libraryId, limit);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get top tags statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
/**
* Get top authors statistics
*/
@GetMapping("/top-authors")
public ResponseEntity<?> getTopAuthorsStatistics(
@PathVariable String libraryId,
@RequestParam(defaultValue = "10") int limit) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getTopAuthorsStatistics(libraryId, limit);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get top authors statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
/**
* Get rating statistics
*/
@GetMapping("/ratings")
public ResponseEntity<?> getRatingStatistics(@PathVariable String libraryId) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getRatingStatistics(libraryId);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get rating statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
/**
* Get source domain statistics
*/
@GetMapping("/source-domains")
public ResponseEntity<?> getSourceDomainStatistics(
@PathVariable String libraryId,
@RequestParam(defaultValue = "10") int limit) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getSourceDomainStatistics(libraryId, limit);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get source domain statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
/**
* Get reading progress statistics
*/
@GetMapping("/reading-progress")
public ResponseEntity<?> getReadingProgressStatistics(@PathVariable String libraryId) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getReadingProgressStatistics(libraryId);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get reading progress statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
/**
* Get reading activity statistics (last week)
*/
@GetMapping("/reading-activity")
public ResponseEntity<?> getReadingActivityStatistics(@PathVariable String libraryId) {
try {
if (libraryService.getLibraryById(libraryId) == null) {
return ResponseEntity.notFound().build();
}
var stats = statisticsService.getReadingActivityStatistics(libraryId);
return ResponseEntity.ok(stats);
} catch (Exception e) {
logger.error("Failed to get reading activity statistics for library: {}", libraryId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("Failed to retrieve statistics: " + e.getMessage()));
}
}
// Error response DTO
private static class ErrorResponse {
private String error;

View File

@@ -0,0 +1,45 @@
package com.storycove.dto;
import java.util.Map;
public class RatingStatsDto {
private double averageRating;
private long totalRatedStories;
private long totalUnratedStories;
private Map<Integer, Long> ratingDistribution; // rating (1-5) -> count
public RatingStatsDto() {
}
public double getAverageRating() {
return averageRating;
}
public void setAverageRating(double averageRating) {
this.averageRating = averageRating;
}
public long getTotalRatedStories() {
return totalRatedStories;
}
public void setTotalRatedStories(long totalRatedStories) {
this.totalRatedStories = totalRatedStories;
}
public long getTotalUnratedStories() {
return totalUnratedStories;
}
public void setTotalUnratedStories(long totalUnratedStories) {
this.totalUnratedStories = totalUnratedStories;
}
public Map<Integer, Long> getRatingDistribution() {
return ratingDistribution;
}
public void setRatingDistribution(Map<Integer, Long> ratingDistribution) {
this.ratingDistribution = ratingDistribution;
}
}

View File

@@ -0,0 +1,84 @@
package com.storycove.dto;
import java.util.List;
public class ReadingActivityStatsDto {
private long storiesReadLastWeek;
private long wordsReadLastWeek;
private long readingTimeMinutesLastWeek;
private List<DailyActivityDto> dailyActivity;
public ReadingActivityStatsDto() {
}
public long getStoriesReadLastWeek() {
return storiesReadLastWeek;
}
public void setStoriesReadLastWeek(long storiesReadLastWeek) {
this.storiesReadLastWeek = storiesReadLastWeek;
}
public long getWordsReadLastWeek() {
return wordsReadLastWeek;
}
public void setWordsReadLastWeek(long wordsReadLastWeek) {
this.wordsReadLastWeek = wordsReadLastWeek;
}
public long getReadingTimeMinutesLastWeek() {
return readingTimeMinutesLastWeek;
}
public void setReadingTimeMinutesLastWeek(long readingTimeMinutesLastWeek) {
this.readingTimeMinutesLastWeek = readingTimeMinutesLastWeek;
}
public List<DailyActivityDto> getDailyActivity() {
return dailyActivity;
}
public void setDailyActivity(List<DailyActivityDto> dailyActivity) {
this.dailyActivity = dailyActivity;
}
public static class DailyActivityDto {
private String date; // YYYY-MM-DD format
private long storiesRead;
private long wordsRead;
public DailyActivityDto() {
}
public DailyActivityDto(String date, long storiesRead, long wordsRead) {
this.date = date;
this.storiesRead = storiesRead;
this.wordsRead = wordsRead;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public long getStoriesRead() {
return storiesRead;
}
public void setStoriesRead(long storiesRead) {
this.storiesRead = storiesRead;
}
public long getWordsRead() {
return wordsRead;
}
public void setWordsRead(long wordsRead) {
this.wordsRead = wordsRead;
}
}
}

View File

@@ -0,0 +1,61 @@
package com.storycove.dto;
public class ReadingProgressStatsDto {
private long totalStories;
private long readStories;
private long unreadStories;
private double percentageRead;
private long totalWordsRead;
private long totalWordsUnread;
public ReadingProgressStatsDto() {
}
public long getTotalStories() {
return totalStories;
}
public void setTotalStories(long totalStories) {
this.totalStories = totalStories;
}
public long getReadStories() {
return readStories;
}
public void setReadStories(long readStories) {
this.readStories = readStories;
}
public long getUnreadStories() {
return unreadStories;
}
public void setUnreadStories(long unreadStories) {
this.unreadStories = unreadStories;
}
public double getPercentageRead() {
return percentageRead;
}
public void setPercentageRead(double percentageRead) {
this.percentageRead = percentageRead;
}
public long getTotalWordsRead() {
return totalWordsRead;
}
public void setTotalWordsRead(long totalWordsRead) {
this.totalWordsRead = totalWordsRead;
}
public long getTotalWordsUnread() {
return totalWordsUnread;
}
public void setTotalWordsUnread(long totalWordsUnread) {
this.totalWordsUnread = totalWordsUnread;
}
}

View File

@@ -0,0 +1,65 @@
package com.storycove.dto;
import java.util.List;
public class SourceDomainStatsDto {
private List<DomainStatsDto> topDomains;
private long storiesWithSource;
private long storiesWithoutSource;
public SourceDomainStatsDto() {
}
public List<DomainStatsDto> getTopDomains() {
return topDomains;
}
public void setTopDomains(List<DomainStatsDto> topDomains) {
this.topDomains = topDomains;
}
public long getStoriesWithSource() {
return storiesWithSource;
}
public void setStoriesWithSource(long storiesWithSource) {
this.storiesWithSource = storiesWithSource;
}
public long getStoriesWithoutSource() {
return storiesWithoutSource;
}
public void setStoriesWithoutSource(long storiesWithoutSource) {
this.storiesWithoutSource = storiesWithoutSource;
}
public static class DomainStatsDto {
private String domain;
private long storyCount;
public DomainStatsDto() {
}
public DomainStatsDto(String domain, long storyCount) {
this.domain = domain;
this.storyCount = storyCount;
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public long getStoryCount() {
return storyCount;
}
public void setStoryCount(long storyCount) {
this.storyCount = storyCount;
}
}
}

View File

@@ -0,0 +1,76 @@
package com.storycove.dto;
import java.util.List;
public class TopAuthorsStatsDto {
private List<AuthorStatsDto> topAuthorsByStories;
private List<AuthorStatsDto> topAuthorsByWords;
public TopAuthorsStatsDto() {
}
public List<AuthorStatsDto> getTopAuthorsByStories() {
return topAuthorsByStories;
}
public void setTopAuthorsByStories(List<AuthorStatsDto> topAuthorsByStories) {
this.topAuthorsByStories = topAuthorsByStories;
}
public List<AuthorStatsDto> getTopAuthorsByWords() {
return topAuthorsByWords;
}
public void setTopAuthorsByWords(List<AuthorStatsDto> topAuthorsByWords) {
this.topAuthorsByWords = topAuthorsByWords;
}
public static class AuthorStatsDto {
private String authorId;
private String authorName;
private long storyCount;
private long totalWords;
public AuthorStatsDto() {
}
public AuthorStatsDto(String authorId, String authorName, long storyCount, long totalWords) {
this.authorId = authorId;
this.authorName = authorName;
this.storyCount = storyCount;
this.totalWords = totalWords;
}
public String getAuthorId() {
return authorId;
}
public void setAuthorId(String authorId) {
this.authorId = authorId;
}
public String getAuthorName() {
return authorName;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public long getStoryCount() {
return storyCount;
}
public void setStoryCount(long storyCount) {
this.storyCount = storyCount;
}
public long getTotalWords() {
return totalWords;
}
public void setTotalWords(long totalWords) {
this.totalWords = totalWords;
}
}
}

View File

@@ -0,0 +1,51 @@
package com.storycove.dto;
import java.util.List;
public class TopTagsStatsDto {
private List<TagStatsDto> topTags;
public TopTagsStatsDto() {
}
public TopTagsStatsDto(List<TagStatsDto> topTags) {
this.topTags = topTags;
}
public List<TagStatsDto> getTopTags() {
return topTags;
}
public void setTopTags(List<TagStatsDto> topTags) {
this.topTags = topTags;
}
public static class TagStatsDto {
private String tagName;
private long storyCount;
public TagStatsDto() {
}
public TagStatsDto(String tagName, long storyCount) {
this.tagName = tagName;
this.storyCount = storyCount;
}
public String getTagName() {
return tagName;
}
public void setTagName(String tagName) {
this.tagName = tagName;
}
public long getStoryCount() {
return storyCount;
}
public void setStoryCount(long storyCount) {
this.storyCount = storyCount;
}
}
}

View File

@@ -1,8 +1,9 @@
package com.storycove.service;
import com.storycove.config.SolrProperties;
import com.storycove.dto.LibraryOverviewStatsDto;
import com.storycove.dto.*;
import com.storycove.dto.LibraryOverviewStatsDto.StoryWordCountDto;
import com.storycove.repository.CollectionRepository;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
@@ -17,7 +18,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@ConditionalOnProperty(
@@ -39,6 +45,9 @@ public class LibraryStatisticsService {
@Autowired
private LibraryService libraryService;
@Autowired
private CollectionRepository collectionRepository;
/**
* Get overview statistics for a library
*/
@@ -133,13 +142,9 @@ public class LibraryStatisticsService {
/**
* Get total number of collections
*/
private long getTotalCollections(String libraryId) throws IOException, SolrServerException {
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.setRows(0);
QueryResponse response = solrClient.query(properties.getCores().getCollections(), query);
return response.getResults().getNumFound();
private long getTotalCollections(String libraryId) {
// Collections are stored in the database, not indexed in Solr
return collectionRepository.countByIsArchivedFalse();
}
/**
@@ -254,4 +259,385 @@ public class LibraryStatisticsService {
long sum = 0;
double mean = 0.0;
}
/**
* Get top tags statistics
*/
public TopTagsStatsDto getTopTagsStatistics(String libraryId, int limit) throws IOException, SolrServerException {
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.setRows(0);
query.setFacet(true);
query.addFacetField("tagNames");
query.setFacetLimit(limit);
query.setFacetSort("count"); // Sort by count (most popular first)
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
FacetField tagsFacet = response.getFacetField("tagNames");
List<TopTagsStatsDto.TagStatsDto> topTags = new ArrayList<>();
if (tagsFacet != null && tagsFacet.getValues() != null) {
for (FacetField.Count count : tagsFacet.getValues()) {
topTags.add(new TopTagsStatsDto.TagStatsDto(count.getName(), count.getCount()));
}
}
return new TopTagsStatsDto(topTags);
}
/**
* Get top authors statistics
*/
public TopAuthorsStatsDto getTopAuthorsStatistics(String libraryId, int limit) throws IOException, SolrServerException {
TopAuthorsStatsDto stats = new TopAuthorsStatsDto();
// Top authors by story count
stats.setTopAuthorsByStories(getTopAuthorsByStoryCount(libraryId, limit));
// Top authors by total words
stats.setTopAuthorsByWords(getTopAuthorsByWordCount(libraryId, limit));
return stats;
}
private List<TopAuthorsStatsDto.AuthorStatsDto> getTopAuthorsByStoryCount(String libraryId, int limit)
throws IOException, SolrServerException {
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.setRows(0);
query.setFacet(true);
query.addFacetField("authorId");
query.setFacetLimit(limit);
query.setFacetSort("count");
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
FacetField authorFacet = response.getFacetField("authorId");
List<TopAuthorsStatsDto.AuthorStatsDto> topAuthors = new ArrayList<>();
if (authorFacet != null && authorFacet.getValues() != null) {
for (FacetField.Count count : authorFacet.getValues()) {
String authorId = count.getName();
long storyCount = count.getCount();
// Get author name and total words
SolrQuery authorQuery = new SolrQuery("authorId:" + authorId);
authorQuery.addFilterQuery("libraryId:" + libraryId);
authorQuery.setRows(1);
authorQuery.setFields("authorName");
QueryResponse authorResponse = solrClient.query(properties.getCores().getStories(), authorQuery);
String authorName = "";
if (!authorResponse.getResults().isEmpty()) {
authorName = (String) authorResponse.getResults().get(0).getFieldValue("authorName");
}
// Get total words for this author
long totalWords = getAuthorTotalWords(libraryId, authorId);
topAuthors.add(new TopAuthorsStatsDto.AuthorStatsDto(authorId, authorName, storyCount, totalWords));
}
}
return topAuthors;
}
private List<TopAuthorsStatsDto.AuthorStatsDto> getTopAuthorsByWordCount(String libraryId, int limit)
throws IOException, SolrServerException {
// First get all unique authors
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.setRows(0);
query.setFacet(true);
query.addFacetField("authorId");
query.setFacetLimit(-1); // Get all authors
query.setFacetSort("count");
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
FacetField authorFacet = response.getFacetField("authorId");
List<TopAuthorsStatsDto.AuthorStatsDto> allAuthors = new ArrayList<>();
if (authorFacet != null && authorFacet.getValues() != null) {
for (FacetField.Count count : authorFacet.getValues()) {
String authorId = count.getName();
long storyCount = count.getCount();
// Get author name
SolrQuery authorQuery = new SolrQuery("authorId:" + authorId);
authorQuery.addFilterQuery("libraryId:" + libraryId);
authorQuery.setRows(1);
authorQuery.setFields("authorName");
QueryResponse authorResponse = solrClient.query(properties.getCores().getStories(), authorQuery);
String authorName = "";
if (!authorResponse.getResults().isEmpty()) {
authorName = (String) authorResponse.getResults().get(0).getFieldValue("authorName");
}
// Get total words for this author
long totalWords = getAuthorTotalWords(libraryId, authorId);
allAuthors.add(new TopAuthorsStatsDto.AuthorStatsDto(authorId, authorName, storyCount, totalWords));
}
}
// Sort by total words and return top N
return allAuthors.stream()
.sorted(Comparator.comparingLong(TopAuthorsStatsDto.AuthorStatsDto::getTotalWords).reversed())
.limit(limit)
.collect(Collectors.toList());
}
private long getAuthorTotalWords(String libraryId, String authorId) throws IOException, SolrServerException {
SolrQuery query = new SolrQuery("authorId:" + authorId);
query.addFilterQuery("libraryId:" + libraryId);
query.setRows(0);
query.setParam(StatsParams.STATS, true);
query.setParam(StatsParams.STATS_FIELD, "wordCount");
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
var fieldStatsInfo = response.getFieldStatsInfo();
if (fieldStatsInfo != null && fieldStatsInfo.get("wordCount") != null) {
var fieldStat = fieldStatsInfo.get("wordCount");
Object sumObj = fieldStat.getSum();
return (sumObj != null) ? ((Number) sumObj).longValue() : 0L;
}
return 0L;
}
/**
* Get rating statistics
*/
public RatingStatsDto getRatingStatistics(String libraryId) throws IOException, SolrServerException {
RatingStatsDto stats = new RatingStatsDto();
// Get average rating using stats component
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.addFilterQuery("rating:[* TO *]"); // Only rated stories
query.setRows(0);
query.setParam(StatsParams.STATS, true);
query.setParam(StatsParams.STATS_FIELD, "rating");
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
long totalRated = response.getResults().getNumFound();
var fieldStatsInfo = response.getFieldStatsInfo();
if (fieldStatsInfo != null && fieldStatsInfo.get("rating") != null) {
var fieldStat = fieldStatsInfo.get("rating");
Object meanObj = fieldStat.getMean();
stats.setAverageRating((meanObj != null) ? ((Number) meanObj).doubleValue() : 0.0);
}
stats.setTotalRatedStories(totalRated);
// Get total stories to calculate unrated
long totalStories = getTotalStories(libraryId);
stats.setTotalUnratedStories(totalStories - totalRated);
// Get rating distribution using faceting
SolrQuery distQuery = new SolrQuery("*:*");
distQuery.addFilterQuery("libraryId:" + libraryId);
distQuery.addFilterQuery("rating:[* TO *]");
distQuery.setRows(0);
distQuery.setFacet(true);
distQuery.addFacetField("rating");
distQuery.setFacetLimit(-1);
QueryResponse distResponse = solrClient.query(properties.getCores().getStories(), distQuery);
FacetField ratingFacet = distResponse.getFacetField("rating");
Map<Integer, Long> distribution = new HashMap<>();
if (ratingFacet != null && ratingFacet.getValues() != null) {
for (FacetField.Count count : ratingFacet.getValues()) {
try {
int rating = Integer.parseInt(count.getName());
distribution.put(rating, count.getCount());
} catch (NumberFormatException e) {
// Skip invalid ratings
}
}
}
stats.setRatingDistribution(distribution);
return stats;
}
/**
* Get source domain statistics
*/
public SourceDomainStatsDto getSourceDomainStatistics(String libraryId, int limit) throws IOException, SolrServerException {
SourceDomainStatsDto stats = new SourceDomainStatsDto();
// Get top domains using faceting
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.addFilterQuery("sourceDomain:[* TO *]"); // Only stories with source
query.setRows(0);
query.setFacet(true);
query.addFacetField("sourceDomain");
query.setFacetLimit(limit);
query.setFacetSort("count");
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
long storiesWithSource = response.getResults().getNumFound();
FacetField domainFacet = response.getFacetField("sourceDomain");
List<SourceDomainStatsDto.DomainStatsDto> topDomains = new ArrayList<>();
if (domainFacet != null && domainFacet.getValues() != null) {
for (FacetField.Count count : domainFacet.getValues()) {
topDomains.add(new SourceDomainStatsDto.DomainStatsDto(count.getName(), count.getCount()));
}
}
stats.setTopDomains(topDomains);
stats.setStoriesWithSource(storiesWithSource);
long totalStories = getTotalStories(libraryId);
stats.setStoriesWithoutSource(totalStories - storiesWithSource);
return stats;
}
/**
* Get reading progress statistics
*/
public ReadingProgressStatsDto getReadingProgressStatistics(String libraryId) throws IOException, SolrServerException {
ReadingProgressStatsDto stats = new ReadingProgressStatsDto();
long totalStories = getTotalStories(libraryId);
stats.setTotalStories(totalStories);
// Get read stories count
SolrQuery readQuery = new SolrQuery("*:*");
readQuery.addFilterQuery("libraryId:" + libraryId);
readQuery.addFilterQuery("isRead:true");
readQuery.setRows(0);
QueryResponse readResponse = solrClient.query(properties.getCores().getStories(), readQuery);
long readStories = readResponse.getResults().getNumFound();
stats.setReadStories(readStories);
stats.setUnreadStories(totalStories - readStories);
if (totalStories > 0) {
stats.setPercentageRead((readStories * 100.0) / totalStories);
}
// Get total words read
SolrQuery readWordsQuery = new SolrQuery("*:*");
readWordsQuery.addFilterQuery("libraryId:" + libraryId);
readWordsQuery.addFilterQuery("isRead:true");
readWordsQuery.setRows(0);
readWordsQuery.setParam(StatsParams.STATS, true);
readWordsQuery.setParam(StatsParams.STATS_FIELD, "wordCount");
QueryResponse readWordsResponse = solrClient.query(properties.getCores().getStories(), readWordsQuery);
var readFieldStats = readWordsResponse.getFieldStatsInfo();
if (readFieldStats != null && readFieldStats.get("wordCount") != null) {
var fieldStat = readFieldStats.get("wordCount");
Object sumObj = fieldStat.getSum();
stats.setTotalWordsRead((sumObj != null) ? ((Number) sumObj).longValue() : 0L);
}
// Get total words unread
SolrQuery unreadWordsQuery = new SolrQuery("*:*");
unreadWordsQuery.addFilterQuery("libraryId:" + libraryId);
unreadWordsQuery.addFilterQuery("isRead:false");
unreadWordsQuery.setRows(0);
unreadWordsQuery.setParam(StatsParams.STATS, true);
unreadWordsQuery.setParam(StatsParams.STATS_FIELD, "wordCount");
QueryResponse unreadWordsResponse = solrClient.query(properties.getCores().getStories(), unreadWordsQuery);
var unreadFieldStats = unreadWordsResponse.getFieldStatsInfo();
if (unreadFieldStats != null && unreadFieldStats.get("wordCount") != null) {
var fieldStat = unreadFieldStats.get("wordCount");
Object sumObj = fieldStat.getSum();
stats.setTotalWordsUnread((sumObj != null) ? ((Number) sumObj).longValue() : 0L);
}
return stats;
}
/**
* Get reading activity statistics for the last week
*/
public ReadingActivityStatsDto getReadingActivityStatistics(String libraryId) throws IOException, SolrServerException {
ReadingActivityStatsDto stats = new ReadingActivityStatsDto();
LocalDateTime oneWeekAgo = LocalDateTime.now().minusWeeks(1);
String oneWeekAgoStr = oneWeekAgo.toInstant(ZoneOffset.UTC).toString();
// Get stories read in last week
SolrQuery query = new SolrQuery("*:*");
query.addFilterQuery("libraryId:" + libraryId);
query.addFilterQuery("lastReadAt:[" + oneWeekAgoStr + " TO *]");
query.setRows(0);
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
long storiesReadLastWeek = response.getResults().getNumFound();
stats.setStoriesReadLastWeek(storiesReadLastWeek);
// Get words read in last week
SolrQuery wordsQuery = new SolrQuery("*:*");
wordsQuery.addFilterQuery("libraryId:" + libraryId);
wordsQuery.addFilterQuery("lastReadAt:[" + oneWeekAgoStr + " TO *]");
wordsQuery.setRows(0);
wordsQuery.setParam(StatsParams.STATS, true);
wordsQuery.setParam(StatsParams.STATS_FIELD, "wordCount");
QueryResponse wordsResponse = solrClient.query(properties.getCores().getStories(), wordsQuery);
var fieldStatsInfo = wordsResponse.getFieldStatsInfo();
long wordsReadLastWeek = 0L;
if (fieldStatsInfo != null && fieldStatsInfo.get("wordCount") != null) {
var fieldStat = fieldStatsInfo.get("wordCount");
Object sumObj = fieldStat.getSum();
wordsReadLastWeek = (sumObj != null) ? ((Number) sumObj).longValue() : 0L;
}
stats.setWordsReadLastWeek(wordsReadLastWeek);
stats.setReadingTimeMinutesLastWeek(wordsReadLastWeek / WORDS_PER_MINUTE);
// Get daily activity (last 7 days)
List<ReadingActivityStatsDto.DailyActivityDto> dailyActivity = new ArrayList<>();
for (int i = 6; i >= 0; i--) {
LocalDate date = LocalDate.now().minusDays(i);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.atTime(23, 59, 59);
String dayStartStr = dayStart.toInstant(ZoneOffset.UTC).toString();
String dayEndStr = dayEnd.toInstant(ZoneOffset.UTC).toString();
SolrQuery dayQuery = new SolrQuery("*:*");
dayQuery.addFilterQuery("libraryId:" + libraryId);
dayQuery.addFilterQuery("lastReadAt:[" + dayStartStr + " TO " + dayEndStr + "]");
dayQuery.setRows(0);
dayQuery.setParam(StatsParams.STATS, true);
dayQuery.setParam(StatsParams.STATS_FIELD, "wordCount");
QueryResponse dayResponse = solrClient.query(properties.getCores().getStories(), dayQuery);
long storiesRead = dayResponse.getResults().getNumFound();
long wordsRead = 0L;
var dayFieldStats = dayResponse.getFieldStatsInfo();
if (dayFieldStats != null && dayFieldStats.get("wordCount") != null) {
var fieldStat = dayFieldStats.get("wordCount");
Object sumObj = fieldStat.getSum();
wordsRead = (sumObj != null) ? ((Number) sumObj).longValue() : 0L;
}
dailyActivity.add(new ReadingActivityStatsDto.DailyActivityDto(
date.format(DateTimeFormatter.ISO_LOCAL_DATE),
storiesRead,
wordsRead
));
}
stats.setDailyActivity(dailyActivity);
return stats;
}
}