2021-01-04 19:29:49 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-07-01 19:30:41 +00:00
|
|
|
"bytes"
|
2024-04-22 13:49:22 +00:00
|
|
|
"cmp"
|
2021-04-28 18:03:20 +00:00
|
|
|
"database/sql"
|
2021-07-01 19:30:41 +00:00
|
|
|
"encoding/gob"
|
2021-01-04 19:29:49 +00:00
|
|
|
"net/http"
|
2022-02-22 15:52:03 +00:00
|
|
|
|
|
|
|
"go.goblog.app/app/pkgs/bufferpool"
|
2021-01-04 19:29:49 +00:00
|
|
|
)
|
|
|
|
|
2021-07-27 10:51:08 +00:00
|
|
|
const (
|
|
|
|
defaultBlogStatsPath = "/statistics"
|
|
|
|
blogStatsTablePath = ".table.html"
|
|
|
|
)
|
2021-07-12 14:19:28 +00:00
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) initBlogStats() {
|
2021-05-10 15:37:34 +00:00
|
|
|
f := func(p *post) {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.db.resetBlogStats(p.Blog)
|
2021-05-10 15:37:34 +00:00
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
a.pPostHooks = append(a.pPostHooks, f)
|
|
|
|
a.pUpdateHooks = append(a.pUpdateHooks, f)
|
|
|
|
a.pDeleteHooks = append(a.pDeleteHooks, f)
|
2022-01-03 12:55:44 +00:00
|
|
|
a.pUndeleteHooks = append(a.pUndeleteHooks, f)
|
2021-05-10 15:37:34 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
2021-12-20 13:00:11 +00:00
|
|
|
_, bc := a.getBlog(r)
|
2024-04-22 13:49:22 +00:00
|
|
|
canonical := bc.getRelativePath(cmp.Or(bc.BlogStats.Path, defaultBlogStatsPath))
|
2022-01-30 15:40:53 +00:00
|
|
|
a.render(w, r, a.renderBlogStats, &renderData{
|
2021-12-20 13:00:11 +00:00
|
|
|
Canonical: a.getFullAddress(canonical),
|
2022-01-20 17:22:10 +00:00
|
|
|
Data: &blogStatsRenderData{
|
|
|
|
tableUrl: canonical + blogStatsTablePath,
|
2021-05-09 07:08:31 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
|
2021-12-20 13:00:11 +00:00
|
|
|
blog, _ := a.getBlog(r)
|
2022-03-16 07:28:03 +00:00
|
|
|
data, err, _ := a.blogStatsCacheGroup.Do(blog, func() (any, error) {
|
2021-06-06 12:39:42 +00:00
|
|
|
return a.db.getBlogStats(blog)
|
2021-05-09 07:35:37 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
2021-05-09 07:35:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// Render
|
2022-01-30 15:40:53 +00:00
|
|
|
a.render(w, r, a.renderBlogStatsTable, &renderData{
|
2021-12-20 13:00:11 +00:00
|
|
|
Data: data,
|
2021-05-09 07:35:37 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-01 16:51:04 +00:00
|
|
|
const blogStatsSql = `
|
|
|
|
with filtered as (
|
|
|
|
select
|
2024-01-03 17:23:44 +00:00
|
|
|
(CASE WHEN coalesce(pub, '') != '' THEN substr(pub, 1, 4) ELSE 'N' END) as year,
|
|
|
|
(CASE WHEN coalesce(pub, '') != '' THEN substr(pub, 6, 2) ELSE 'N' END) as month,
|
2021-07-01 19:30:41 +00:00
|
|
|
wordcount(content) as words,
|
|
|
|
charcount(content) as chars
|
|
|
|
from (
|
|
|
|
select
|
2021-07-14 13:44:57 +00:00
|
|
|
tolocal(published) as pub,
|
2021-07-01 19:30:41 +00:00
|
|
|
mdtext(coalesce(content, '')) as content
|
|
|
|
from posts
|
2022-09-23 09:05:07 +00:00
|
|
|
where status = @status and visibility = @visibility and blog = @blog
|
2021-07-01 19:30:41 +00:00
|
|
|
)
|
2024-01-03 17:23:44 +00:00
|
|
|
), aggregated as (
|
|
|
|
select
|
|
|
|
year,
|
|
|
|
month,
|
|
|
|
coalesce(count(*), 0) as pc,
|
|
|
|
coalesce(sum(words), 0) as wc,
|
|
|
|
coalesce(sum(chars), 0) as cc,
|
|
|
|
coalesce(round(avg(words), 0), 0) as wpp
|
|
|
|
from filtered
|
|
|
|
group by year, month
|
2021-07-01 16:51:04 +00:00
|
|
|
)
|
|
|
|
select *
|
|
|
|
from (
|
|
|
|
select *
|
|
|
|
from (
|
2024-01-03 17:23:44 +00:00
|
|
|
select year, 'A', sum(pc), sum(wc), sum(cc), round(sum(wc)/sum(pc), 0)
|
|
|
|
from aggregated
|
|
|
|
where year != 'N'
|
2021-07-01 16:51:04 +00:00
|
|
|
group by year
|
|
|
|
order by year desc
|
|
|
|
)
|
|
|
|
union all
|
|
|
|
select *
|
|
|
|
from (
|
2024-01-03 17:23:44 +00:00
|
|
|
select *
|
|
|
|
from aggregated
|
|
|
|
where year != 'N'
|
2021-07-01 16:51:04 +00:00
|
|
|
order by year desc, month desc
|
|
|
|
)
|
|
|
|
union all
|
|
|
|
select *
|
|
|
|
from (
|
2024-01-03 17:23:44 +00:00
|
|
|
select *
|
|
|
|
from aggregated
|
|
|
|
where year == 'N'
|
2021-07-01 16:51:04 +00:00
|
|
|
)
|
|
|
|
union all
|
|
|
|
select *
|
|
|
|
from (
|
2024-01-03 17:23:44 +00:00
|
|
|
select 'A', 'A', sum(pc), sum(wc), sum(cc), round(sum(wc)/sum(pc), 0)
|
|
|
|
from aggregated
|
2021-07-01 16:51:04 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
`
|
|
|
|
|
2021-07-01 19:30:41 +00:00
|
|
|
type blogStatsRow struct {
|
|
|
|
Name, Posts, Chars, Words, WordsPerPost string
|
|
|
|
}
|
|
|
|
|
|
|
|
type blogStatsData struct {
|
|
|
|
Total blogStatsRow
|
|
|
|
NoDate blogStatsRow
|
|
|
|
Years []blogStatsRow
|
|
|
|
Months map[string][]blogStatsRow
|
|
|
|
}
|
|
|
|
|
|
|
|
func (db *database) getBlogStats(blog string) (data *blogStatsData, err error) {
|
2021-07-01 16:51:04 +00:00
|
|
|
// Check cache
|
2021-06-06 12:39:42 +00:00
|
|
|
if stats := db.loadBlogStatsCache(blog); stats != nil {
|
2021-05-10 15:37:34 +00:00
|
|
|
return stats, nil
|
2021-05-09 07:35:37 +00:00
|
|
|
}
|
2021-07-01 16:51:04 +00:00
|
|
|
// Prevent creating posts while getting stats
|
|
|
|
db.pcm.Lock()
|
|
|
|
defer db.pcm.Unlock()
|
|
|
|
// Scan objects
|
2021-07-01 19:30:41 +00:00
|
|
|
currentStats := blogStatsRow{}
|
2021-07-01 16:51:04 +00:00
|
|
|
var currentMonth, currentYear string
|
|
|
|
// Data to later return
|
2021-07-01 19:30:41 +00:00
|
|
|
data = &blogStatsData{
|
|
|
|
Months: map[string][]blogStatsRow{},
|
|
|
|
}
|
2021-07-01 16:51:04 +00:00
|
|
|
// Query and scan
|
2022-09-23 09:05:07 +00:00
|
|
|
rows, err := db.Query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("visibility", visibilityPublic), sql.Named("blog", blog))
|
2021-04-23 18:52:12 +00:00
|
|
|
if err != nil {
|
2021-05-09 07:35:37 +00:00
|
|
|
return nil, err
|
2021-04-23 18:52:12 +00:00
|
|
|
}
|
2021-07-01 16:51:04 +00:00
|
|
|
for rows.Next() {
|
|
|
|
err = rows.Scan(¤tYear, ¤tMonth, ¤tStats.Posts, ¤tStats.Words, ¤tStats.Chars, ¤tStats.WordsPerPost)
|
|
|
|
if currentYear == "A" && currentMonth == "A" {
|
2021-07-01 19:30:41 +00:00
|
|
|
data.Total = blogStatsRow{
|
2021-07-01 16:51:04 +00:00
|
|
|
Posts: currentStats.Posts,
|
|
|
|
Words: currentStats.Words,
|
|
|
|
Chars: currentStats.Chars,
|
|
|
|
WordsPerPost: currentStats.WordsPerPost,
|
2021-04-23 18:52:12 +00:00
|
|
|
}
|
2021-07-01 16:51:04 +00:00
|
|
|
} else if currentYear == "N" && currentMonth == "N" {
|
2021-07-01 19:30:41 +00:00
|
|
|
data.NoDate = blogStatsRow{
|
2021-07-01 16:51:04 +00:00
|
|
|
Posts: currentStats.Posts,
|
|
|
|
Words: currentStats.Words,
|
|
|
|
Chars: currentStats.Chars,
|
|
|
|
WordsPerPost: currentStats.WordsPerPost,
|
|
|
|
}
|
|
|
|
} else if currentMonth == "A" {
|
2021-07-01 19:30:41 +00:00
|
|
|
data.Years = append(data.Years, blogStatsRow{
|
2021-07-01 16:51:04 +00:00
|
|
|
Name: currentYear,
|
|
|
|
Posts: currentStats.Posts,
|
|
|
|
Words: currentStats.Words,
|
|
|
|
Chars: currentStats.Chars,
|
|
|
|
WordsPerPost: currentStats.WordsPerPost,
|
|
|
|
})
|
|
|
|
} else {
|
2021-07-01 19:30:41 +00:00
|
|
|
data.Months[currentYear] = append(data.Months[currentYear], blogStatsRow{
|
2021-07-01 16:51:04 +00:00
|
|
|
Name: currentMonth,
|
|
|
|
Posts: currentStats.Posts,
|
|
|
|
Words: currentStats.Words,
|
|
|
|
Chars: currentStats.Chars,
|
|
|
|
WordsPerPost: currentStats.WordsPerPost,
|
|
|
|
})
|
2021-01-04 19:29:49 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
db.cacheBlogStats(blog, data)
|
2021-05-09 07:35:37 +00:00
|
|
|
return data, nil
|
|
|
|
}
|
|
|
|
|
2021-07-01 19:30:41 +00:00
|
|
|
func (db *database) cacheBlogStats(blog string, stats *blogStatsData) {
|
2022-02-22 15:52:03 +00:00
|
|
|
buf := bufferpool.Get()
|
|
|
|
_ = gob.NewEncoder(buf).Encode(stats)
|
2021-07-01 19:30:41 +00:00
|
|
|
_ = db.cachePersistently("blogstats_"+blog, buf.Bytes())
|
2022-02-22 15:52:03 +00:00
|
|
|
bufferpool.Put(buf)
|
2021-05-10 15:37:34 +00:00
|
|
|
}
|
|
|
|
|
2021-07-01 19:30:41 +00:00
|
|
|
func (db *database) loadBlogStatsCache(blog string) (stats *blogStatsData) {
|
2021-06-06 12:39:42 +00:00
|
|
|
data, err := db.retrievePersistentCache("blogstats_" + blog)
|
2021-05-10 15:37:34 +00:00
|
|
|
if err != nil || data == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2021-07-01 19:30:41 +00:00
|
|
|
stats = &blogStatsData{}
|
|
|
|
err = gob.NewDecoder(bytes.NewReader(data)).Decode(stats)
|
2021-05-10 15:37:34 +00:00
|
|
|
if err != nil {
|
2021-07-01 19:30:41 +00:00
|
|
|
return nil
|
2021-05-10 15:37:34 +00:00
|
|
|
}
|
|
|
|
return stats
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (db *database) resetBlogStats(blog string) {
|
|
|
|
_ = db.clearPersistentCache("blogstats_" + blog)
|
2021-01-04 19:29:49 +00:00
|
|
|
}
|