initial statistics implementation
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
package com.storycove.controller;
|
||||||
|
|
||||||
|
import com.storycove.dto.LibraryOverviewStatsDto;
|
||||||
|
import com.storycove.service.LibraryService;
|
||||||
|
import com.storycove.service.LibraryStatisticsService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/libraries/{libraryId}/statistics")
|
||||||
|
public class LibraryStatisticsController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LibraryStatisticsController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LibraryStatisticsService statisticsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LibraryService libraryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get overview statistics for a library
|
||||||
|
*/
|
||||||
|
@GetMapping("/overview")
|
||||||
|
public ResponseEntity<?> getOverviewStatistics(@PathVariable String libraryId) {
|
||||||
|
try {
|
||||||
|
// Verify library exists
|
||||||
|
if (libraryService.getLibraryById(libraryId) == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryOverviewStatsDto stats = statisticsService.getOverviewStatistics(libraryId);
|
||||||
|
return ResponseEntity.ok(stats);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to get overview 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;
|
||||||
|
|
||||||
|
public ErrorResponse(String error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package com.storycove.dto;
|
||||||
|
|
||||||
|
public class LibraryOverviewStatsDto {
|
||||||
|
|
||||||
|
// Collection Overview
|
||||||
|
private long totalStories;
|
||||||
|
private long totalAuthors;
|
||||||
|
private long totalSeries;
|
||||||
|
private long totalTags;
|
||||||
|
private long totalCollections;
|
||||||
|
private long uniqueSourceDomains;
|
||||||
|
|
||||||
|
// Content Metrics
|
||||||
|
private long totalWordCount;
|
||||||
|
private double averageWordsPerStory;
|
||||||
|
private StoryWordCountDto longestStory;
|
||||||
|
private StoryWordCountDto shortestStory;
|
||||||
|
|
||||||
|
// Reading Time (based on 250 words/minute)
|
||||||
|
private long totalReadingTimeMinutes;
|
||||||
|
private double averageReadingTimeMinutes;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
public LibraryOverviewStatsDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public long getTotalStories() {
|
||||||
|
return totalStories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalStories(long totalStories) {
|
||||||
|
this.totalStories = totalStories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalAuthors() {
|
||||||
|
return totalAuthors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalAuthors(long totalAuthors) {
|
||||||
|
this.totalAuthors = totalAuthors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalSeries() {
|
||||||
|
return totalSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalSeries(long totalSeries) {
|
||||||
|
this.totalSeries = totalSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalTags() {
|
||||||
|
return totalTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalTags(long totalTags) {
|
||||||
|
this.totalTags = totalTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalCollections() {
|
||||||
|
return totalCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalCollections(long totalCollections) {
|
||||||
|
this.totalCollections = totalCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUniqueSourceDomains() {
|
||||||
|
return uniqueSourceDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUniqueSourceDomains(long uniqueSourceDomains) {
|
||||||
|
this.uniqueSourceDomains = uniqueSourceDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalWordCount() {
|
||||||
|
return totalWordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalWordCount(long totalWordCount) {
|
||||||
|
this.totalWordCount = totalWordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageWordsPerStory() {
|
||||||
|
return averageWordsPerStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAverageWordsPerStory(double averageWordsPerStory) {
|
||||||
|
this.averageWordsPerStory = averageWordsPerStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoryWordCountDto getLongestStory() {
|
||||||
|
return longestStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongestStory(StoryWordCountDto longestStory) {
|
||||||
|
this.longestStory = longestStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoryWordCountDto getShortestStory() {
|
||||||
|
return shortestStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShortestStory(StoryWordCountDto shortestStory) {
|
||||||
|
this.shortestStory = shortestStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalReadingTimeMinutes() {
|
||||||
|
return totalReadingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalReadingTimeMinutes(long totalReadingTimeMinutes) {
|
||||||
|
this.totalReadingTimeMinutes = totalReadingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageReadingTimeMinutes() {
|
||||||
|
return averageReadingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAverageReadingTimeMinutes(double averageReadingTimeMinutes) {
|
||||||
|
this.averageReadingTimeMinutes = averageReadingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested DTO for story word count info
|
||||||
|
public static class StoryWordCountDto {
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private String authorName;
|
||||||
|
private int wordCount;
|
||||||
|
private long readingTimeMinutes;
|
||||||
|
|
||||||
|
public StoryWordCountDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoryWordCountDto(String id, String title, String authorName, int wordCount, long readingTimeMinutes) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.authorName = authorName;
|
||||||
|
this.wordCount = wordCount;
|
||||||
|
this.readingTimeMinutes = readingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorName() {
|
||||||
|
return authorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthorName(String authorName) {
|
||||||
|
this.authorName = authorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWordCount() {
|
||||||
|
return wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWordCount(int wordCount) {
|
||||||
|
this.wordCount = wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getReadingTimeMinutes() {
|
||||||
|
return readingTimeMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadingTimeMinutes(long readingTimeMinutes) {
|
||||||
|
this.readingTimeMinutes = readingTimeMinutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package com.storycove.service;
|
||||||
|
|
||||||
|
import com.storycove.config.SolrProperties;
|
||||||
|
import com.storycove.dto.LibraryOverviewStatsDto;
|
||||||
|
import com.storycove.dto.LibraryOverviewStatsDto.StoryWordCountDto;
|
||||||
|
import org.apache.solr.client.solrj.SolrClient;
|
||||||
|
import org.apache.solr.client.solrj.SolrQuery;
|
||||||
|
import org.apache.solr.client.solrj.SolrServerException;
|
||||||
|
import org.apache.solr.client.solrj.response.FacetField;
|
||||||
|
import org.apache.solr.client.solrj.response.QueryResponse;
|
||||||
|
import org.apache.solr.common.SolrDocument;
|
||||||
|
import org.apache.solr.common.params.StatsParams;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "storycove.search.engine",
|
||||||
|
havingValue = "solr",
|
||||||
|
matchIfMissing = false
|
||||||
|
)
|
||||||
|
public class LibraryStatisticsService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LibraryStatisticsService.class);
|
||||||
|
private static final int WORDS_PER_MINUTE = 250;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private SolrClient solrClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SolrProperties properties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LibraryService libraryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get overview statistics for a library
|
||||||
|
*/
|
||||||
|
public LibraryOverviewStatsDto getOverviewStatistics(String libraryId) throws IOException, SolrServerException {
|
||||||
|
LibraryOverviewStatsDto stats = new LibraryOverviewStatsDto();
|
||||||
|
|
||||||
|
// Collection Overview
|
||||||
|
stats.setTotalStories(getTotalStories(libraryId));
|
||||||
|
stats.setTotalAuthors(getTotalAuthors(libraryId));
|
||||||
|
stats.setTotalSeries(getTotalSeries(libraryId));
|
||||||
|
stats.setTotalTags(getTotalTags(libraryId));
|
||||||
|
stats.setTotalCollections(getTotalCollections(libraryId));
|
||||||
|
stats.setUniqueSourceDomains(getUniqueSourceDomains(libraryId));
|
||||||
|
|
||||||
|
// Content Metrics - use Solr Stats Component
|
||||||
|
WordCountStats wordStats = getWordCountStatistics(libraryId);
|
||||||
|
stats.setTotalWordCount(wordStats.sum);
|
||||||
|
stats.setAverageWordsPerStory(wordStats.mean);
|
||||||
|
stats.setLongestStory(getLongestStory(libraryId));
|
||||||
|
stats.setShortestStory(getShortestStory(libraryId));
|
||||||
|
|
||||||
|
// Reading Time
|
||||||
|
stats.setTotalReadingTimeMinutes(wordStats.sum / WORDS_PER_MINUTE);
|
||||||
|
stats.setAverageReadingTimeMinutes(wordStats.mean / WORDS_PER_MINUTE);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of stories in library
|
||||||
|
*/
|
||||||
|
private long getTotalStories(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.setRows(0); // We only want the count
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
return response.getResults().getNumFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of authors in library
|
||||||
|
*/
|
||||||
|
private long getTotalAuthors(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.setRows(0);
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getAuthors(), query);
|
||||||
|
return response.getResults().getNumFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of series using faceting on seriesId
|
||||||
|
*/
|
||||||
|
private long getTotalSeries(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.addFilterQuery("seriesId:[* TO *]"); // Only stories that have a series
|
||||||
|
query.setRows(0);
|
||||||
|
query.setFacet(true);
|
||||||
|
query.addFacetField("seriesId");
|
||||||
|
query.setFacetLimit(-1); // Get all unique series
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
FacetField seriesFacet = response.getFacetField("seriesId");
|
||||||
|
|
||||||
|
return (seriesFacet != null && seriesFacet.getValues() != null)
|
||||||
|
? seriesFacet.getValueCount()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of unique tags using faceting
|
||||||
|
*/
|
||||||
|
private long getTotalTags(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.setRows(0);
|
||||||
|
query.setFacet(true);
|
||||||
|
query.addFacetField("tagNames");
|
||||||
|
query.setFacetLimit(-1); // Get all unique tags
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
FacetField tagsFacet = response.getFacetField("tagNames");
|
||||||
|
|
||||||
|
return (tagsFacet != null && tagsFacet.getValues() != null)
|
||||||
|
? tagsFacet.getValueCount()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get number of unique source domains using faceting
|
||||||
|
*/
|
||||||
|
private long getUniqueSourceDomains(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.addFilterQuery("sourceDomain:[* TO *]"); // Only stories with a source domain
|
||||||
|
query.setRows(0);
|
||||||
|
query.setFacet(true);
|
||||||
|
query.addFacetField("sourceDomain");
|
||||||
|
query.setFacetLimit(-1);
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
FacetField domainFacet = response.getFacetField("sourceDomain");
|
||||||
|
|
||||||
|
return (domainFacet != null && domainFacet.getValues() != null)
|
||||||
|
? domainFacet.getValueCount()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get word count statistics using Solr Stats Component
|
||||||
|
*/
|
||||||
|
private WordCountStats getWordCountStatistics(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
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);
|
||||||
|
|
||||||
|
WordCountStats stats = new WordCountStats();
|
||||||
|
|
||||||
|
// Extract stats from response
|
||||||
|
var fieldStatsInfo = response.getFieldStatsInfo();
|
||||||
|
if (fieldStatsInfo != null && fieldStatsInfo.get("wordCount") != null) {
|
||||||
|
var fieldStat = fieldStatsInfo.get("wordCount");
|
||||||
|
|
||||||
|
Object sumObj = fieldStat.getSum();
|
||||||
|
Object meanObj = fieldStat.getMean();
|
||||||
|
|
||||||
|
stats.sum = (sumObj != null) ? ((Number) sumObj).longValue() : 0L;
|
||||||
|
stats.mean = (meanObj != null) ? ((Number) meanObj).doubleValue() : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the longest story in the library
|
||||||
|
*/
|
||||||
|
private StoryWordCountDto getLongestStory(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.addFilterQuery("wordCount:[1 TO *]"); // Exclude stories with 0 words
|
||||||
|
query.setSort("wordCount", SolrQuery.ORDER.desc);
|
||||||
|
query.setRows(1);
|
||||||
|
query.setFields("id", "title", "authorName", "wordCount");
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
|
||||||
|
if (response.getResults().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SolrDocument doc = response.getResults().get(0);
|
||||||
|
return createStoryWordCountDto(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shortest story in the library (excluding 0 word count)
|
||||||
|
*/
|
||||||
|
private StoryWordCountDto getShortestStory(String libraryId) throws IOException, SolrServerException {
|
||||||
|
SolrQuery query = new SolrQuery("*:*");
|
||||||
|
query.addFilterQuery("libraryId:" + libraryId);
|
||||||
|
query.addFilterQuery("wordCount:[1 TO *]"); // Exclude stories with 0 words
|
||||||
|
query.setSort("wordCount", SolrQuery.ORDER.asc);
|
||||||
|
query.setRows(1);
|
||||||
|
query.setFields("id", "title", "authorName", "wordCount");
|
||||||
|
|
||||||
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), query);
|
||||||
|
|
||||||
|
if (response.getResults().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SolrDocument doc = response.getResults().get(0);
|
||||||
|
return createStoryWordCountDto(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create StoryWordCountDto from Solr document
|
||||||
|
*/
|
||||||
|
private StoryWordCountDto createStoryWordCountDto(SolrDocument doc) {
|
||||||
|
String id = (String) doc.getFieldValue("id");
|
||||||
|
String title = (String) doc.getFieldValue("title");
|
||||||
|
String authorName = (String) doc.getFieldValue("authorName");
|
||||||
|
Object wordCountObj = doc.getFieldValue("wordCount");
|
||||||
|
int wordCount = (wordCountObj != null) ? ((Number) wordCountObj).intValue() : 0;
|
||||||
|
long readingTime = wordCount / WORDS_PER_MINUTE;
|
||||||
|
|
||||||
|
return new StoryWordCountDto(id, title, authorName, wordCount, readingTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to hold word count statistics
|
||||||
|
*/
|
||||||
|
private static class WordCountStats {
|
||||||
|
long sum = 0;
|
||||||
|
double mean = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -385,9 +385,69 @@ public class SolrService {
|
|||||||
logger.warn("Could not add libraryId field to document (field may not exist in schema): {}", e.getMessage());
|
logger.warn("Could not add libraryId field to document (field may not exist in schema): {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add derived fields for statistics (Phase 1)
|
||||||
|
addDerivedStatisticsFields(doc, story);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add derived fields to support statistics queries
|
||||||
|
*/
|
||||||
|
private void addDerivedStatisticsFields(SolrInputDocument doc, Story story) {
|
||||||
|
try {
|
||||||
|
// Boolean flags for filtering
|
||||||
|
doc.addField("hasDescription", story.getDescription() != null && !story.getDescription().trim().isEmpty());
|
||||||
|
doc.addField("hasCoverImage", story.getCoverPath() != null && !story.getCoverPath().trim().isEmpty());
|
||||||
|
doc.addField("hasRating", story.getRating() != null && story.getRating() > 0);
|
||||||
|
|
||||||
|
// Extract source domain from URL
|
||||||
|
if (story.getSourceUrl() != null && !story.getSourceUrl().trim().isEmpty()) {
|
||||||
|
String domain = extractDomain(story.getSourceUrl());
|
||||||
|
if (domain != null) {
|
||||||
|
doc.addField("sourceDomain", domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag count for statistics
|
||||||
|
int tagCount = (story.getTags() != null) ? story.getTags().size() : 0;
|
||||||
|
doc.addField("tagCount", tagCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Don't fail indexing if derived fields can't be added
|
||||||
|
logger.debug("Could not add some derived statistics fields: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract domain from URL for source statistics
|
||||||
|
*/
|
||||||
|
private String extractDomain(String url) {
|
||||||
|
try {
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle URLs without protocol
|
||||||
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.net.URL parsedUrl = new java.net.URL(url);
|
||||||
|
String host = parsedUrl.getHost();
|
||||||
|
|
||||||
|
// Remove www. prefix if present
|
||||||
|
if (host.startsWith("www.")) {
|
||||||
|
host = host.substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return host;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Failed to extract domain from URL: {}", url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SolrInputDocument createAuthorDocument(Author author) {
|
private SolrInputDocument createAuthorDocument(Author author) {
|
||||||
SolrInputDocument doc = new SolrInputDocument();
|
SolrInputDocument doc = new SolrInputDocument();
|
||||||
|
|
||||||
|
|||||||
@@ -1053,6 +1053,14 @@ export const clearLibraryCache = (): void => {
|
|||||||
currentLibraryId = null;
|
currentLibraryId = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Library statistics endpoints
|
||||||
|
export const statisticsApi = {
|
||||||
|
getOverviewStatistics: async (libraryId: string): Promise<import('../types/api').LibraryOverviewStats> => {
|
||||||
|
const response = await api.get(`/libraries/${libraryId}/statistics/overview`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Image utility - now library-aware
|
// Image utility - now library-aware
|
||||||
export const getImageUrl = (path: string): string => {
|
export const getImageUrl = (path: string): string => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
|
|||||||
@@ -205,3 +205,32 @@ export interface FilterPreset {
|
|||||||
filters: Partial<AdvancedFilters>;
|
filters: Partial<AdvancedFilters>;
|
||||||
category: 'length' | 'date' | 'rating' | 'reading' | 'content' | 'organization';
|
category: 'length' | 'date' | 'rating' | 'reading' | 'content' | 'organization';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Library Statistics
|
||||||
|
export interface LibraryOverviewStats {
|
||||||
|
// Collection Overview
|
||||||
|
totalStories: number;
|
||||||
|
totalAuthors: number;
|
||||||
|
totalSeries: number;
|
||||||
|
totalTags: number;
|
||||||
|
totalCollections: number;
|
||||||
|
uniqueSourceDomains: number;
|
||||||
|
|
||||||
|
// Content Metrics
|
||||||
|
totalWordCount: number;
|
||||||
|
averageWordsPerStory: number;
|
||||||
|
longestStory: StoryWordCount | null;
|
||||||
|
shortestStory: StoryWordCount | null;
|
||||||
|
|
||||||
|
// Reading Time
|
||||||
|
totalReadingTimeMinutes: number;
|
||||||
|
averageReadingTimeMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoryWordCount {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
authorName: string;
|
||||||
|
wordCount: number;
|
||||||
|
readingTimeMinutes: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user