Skip to content

Commit

Permalink
Merge pull request #213 from sbondCo/admin-stats
Browse files Browse the repository at this point in the history
Admin stats
  • Loading branch information
IRHM authored Nov 19, 2023
2 parents 6211901 + 20ca9ff commit 23d407f
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 62 deletions.
54 changes: 54 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log"
"log/slog"
"os"

"gorm.io/gorm"
)

type ServerConfig struct {
Expand Down Expand Up @@ -160,3 +162,55 @@ func getEnabledFeatures(userPerms int) ServerFeatures {
}
return f
}

type ServerStats struct {
Users int64 `json:"users"`
PrivateUsers int64 `json:"privateUsers"`
WatchedMovies int64 `json:"watchedMovies"`
WatchedShows int64 `json:"watchedShows"`
WatchedSeasons int64 `json:"watchedSeasons"`
MostWatchedMovie Content `json:"mostWatchedMovie"`
MostWatchedShow Content `json:"mostWatchedShow"`
Activities int64 `json:"activities"`
}

// Collect and return server stats
// I cant sql so this the best yall gettin
func getServerStats(db *gorm.DB) ServerStats {
stats := ServerStats{}
resp := db.Model(&User{}).Count(&stats.Users).Where("private = 1").Count(&stats.PrivateUsers)
if resp.Error != nil {
slog.Error("getServerStats - Users query failed", "error", resp.Error)
}
resp = db.Model(&WatchedSeason{}).Count(&stats.WatchedSeasons)
if resp.Error != nil {
slog.Error("getServerStats - WatchedSeasons query failed", "error", resp.Error)
}
resp = db.Model(&Activity{}).Count(&stats.Activities)
if resp.Error != nil {
slog.Error("getServerStats - Activities query failed", "error", resp.Error)
}
resp = db.Joins("JOIN contents ON contents.id = watcheds.content_id AND contents.type = ?", "tv").Find(&Watched{}).Count(&stats.WatchedShows)
if resp.Error != nil {
slog.Error("getServerStats - WatchedShows query failed", "error", resp.Error)
}
resp = db.Joins("JOIN contents ON contents.id = watcheds.content_id AND contents.type = ?", "movie").Find(&Watched{}).Count(&stats.WatchedMovies)
if resp.Error != nil {
slog.Error("getServerStats - WatchedMovies query failed", "error", resp.Error)
}

var w Watched
resp = db.Model(&Watched{}).Select("content_id, COUNT(*) AS mag").Joins("JOIN contents ON contents.type = ? AND contents.id = watcheds.content_id", "tv").Group("content_id").Order("mag DESC").Preload("Content").First(&w)
if resp.Error != nil {
slog.Error("getServerStats - MostWatchedShow query failed", "error", resp.Error)
} else {
stats.MostWatchedShow = w.Content
}
resp = db.Model(&Watched{}).Select("content_id, COUNT(*) AS mag").Joins("JOIN contents ON contents.type = ? AND contents.id = watcheds.content_id", "movie").Group("content_id").Order("mag DESC").Preload("Content").First(&w)
if resp.Error != nil {
slog.Error("getServerStats - MostWatchedMovie query failed", "error", resp.Error)
} else {
stats.MostWatchedMovie = w.Content
}
return stats
}
34 changes: 20 additions & 14 deletions server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ type KeyValueRequest struct {
type BaseRouter struct {
db *gorm.DB
rg *gin.RouterGroup
ms *persistence.InMemoryStore
}

func newBaseRouter(db *gorm.DB, rg *gin.RouterGroup) *BaseRouter {
return &BaseRouter{
db: db,
rg: rg,
ms: persistence.NewInMemoryStore(time.Hour * 24),
}
}

Expand Down Expand Up @@ -70,10 +72,9 @@ func (b *BaseRouter) addSetupRoutes() {
func (b *BaseRouter) addContentRoutes() {
content := b.rg.Group("/content").Use(AuthRequired(nil))
exp := time.Hour * 24
store := persistence.NewInMemoryStore(exp)

// Search for content
content.GET("/:query", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
// println(c.Param("query"))
if c.Param("query") == "" {
c.Status(400)
Expand All @@ -88,7 +89,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get movie details (for movie page)
content.Use(WhereaboutsRequired()).GET("/movie/:id", cache.CachePage(store, exp, func(c *gin.Context) {
content.Use(WhereaboutsRequired()).GET("/movie/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -102,7 +103,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get movie cast
content.GET("/movie/:id/credits", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/movie/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -116,7 +117,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get tv details (for tv page)
content.Use(WhereaboutsRequired()).GET("/tv/:id", cache.CachePage(store, exp, func(c *gin.Context) {
content.Use(WhereaboutsRequired()).GET("/tv/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -130,7 +131,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get tv cast
content.GET("/tv/:id/credits", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/tv/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -144,7 +145,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get season details
content.GET("/tv/:id/season/:num", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/tv/:id/season/:num", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" || c.Param("num") == "" {
c.Status(400)
return
Expand All @@ -158,7 +159,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get person details
content.GET("/person/:id", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/person/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -172,7 +173,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get person credits
content.GET("/person/:id/credits", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/person/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
Expand All @@ -186,7 +187,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Discover movies
content.GET("/discover/movies", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/discover/movies", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := discoverMovies()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
Expand All @@ -196,7 +197,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Discover shows
content.GET("/discover/tv", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/discover/tv", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := discoverTv()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
Expand All @@ -206,7 +207,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Get all trending (movies, tv, people)
content.GET("/trending", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/trending", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := allTrending()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
Expand All @@ -216,7 +217,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Upcoming Movies
content.GET("/upcoming/movies", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/upcoming/movies", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := upcomingMovies()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
Expand All @@ -226,7 +227,7 @@ func (b *BaseRouter) addContentRoutes() {
}))

// Upcoming Tv
content.GET("/upcoming/tv", cache.CachePage(store, exp, func(c *gin.Context) {
content.GET("/upcoming/tv", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := upcomingTv()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
Expand Down Expand Up @@ -604,6 +605,11 @@ func (b *BaseRouter) addServerRoutes() {
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})

// Get server stats
server.GET("/stats", cache.CachePage(b.ms, time.Minute*5, func(c *gin.Context) {
c.JSON(http.StatusOK, getServerStats(b.db))
}))
}

func (b *BaseRouter) addFeatureRoutes() {
Expand Down
36 changes: 36 additions & 0 deletions src/lib/stats/Stat.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
export let name: string;
export let value: string | number;
export let large = false;
export let href: string | undefined = undefined;
</script>

<a {href}>
<span class={large ? "large" : ""}>{value}</span>
<span>{name}</span>
</a>

<style lang="scss">
a {
display: flex;
flex-flow: column;
flex-grow: 1;
flex: 1 1 115px;
padding: 20px 15px;
background-color: $accent-color;
border-radius: 8px;
> span:first-child {
font-weight: bold;
font-size: 20px;
&.large {
font-size: 32px;
}
}
> span:last-child {
margin-top: auto;
}
}
</style>
13 changes: 13 additions & 0 deletions src/lib/stats/Stats.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div>
<slot />
</div>

<style lang="scss">
div {
display: flex;
flex-flow: row;
gap: 12px;
margin-top: 15px;
flex-wrap: wrap;
}
</style>
54 changes: 7 additions & 47 deletions src/routes/(app)/profile/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import Error from "@/lib/Error.svelte";
import Spinner from "@/lib/Spinner.svelte";
import Setting from "@/lib/settings/Setting.svelte";
import Stat from "@/lib/stats/Stat.svelte";
import Stats from "@/lib/stats/Stats.svelte";
import { updateUserSetting } from "@/lib/util/api";
import { getOrdinalSuffix, monthsShort, toggleTheme } from "@/lib/util/helpers";
import { appTheme, userInfo, userSettings } from "@/store";
Expand Down Expand Up @@ -32,26 +34,17 @@
<div class="inner">
<h2 title={user?.username}>Hey {user?.username}</h2>

<div class="stats">
<Stats>
{#await getProfile()}
<Spinner />
{:then profile}
<div>
<span>{formatDate(new Date(profile.joined))}</span>
<span>Joined</span>
</div>
<div>
<span class="large">{profile.moviesWatched}</span>
<span>Movies Watched</span>
</div>
<div>
<span class="large">{profile.showsWatched}</span>
<span>Shows Watched</span>
</div>
<Stat name="Joined" value={formatDate(new Date(profile.joined))} />
<Stat name="Movies Watched" value={profile.moviesWatched} large />
<Stat name="Shows Watched" value={profile.showsWatched} large />
{:catch err}
<Error error={err} pretty="Failed to get stats!" />
{/await}
</div>
</Stats>

<div class="settings">
<h3 class="norm">Settings</h3>
Expand Down Expand Up @@ -140,39 +133,6 @@
}
}
.stats {
display: flex;
flex-flow: row;
gap: 12px;
margin-top: 15px;
@media screen and (max-width: 440px) {
flex-wrap: wrap;
}
> div {
display: flex;
flex-flow: column;
flex-grow: 1;
padding: 20px 15px;
background-color: $accent-color;
border-radius: 8px;
> span:first-child {
font-weight: bold;
font-size: 20px;
&.large {
font-size: 32px;
}
}
> span:last-child {
margin-top: auto;
}
}
}
.settings {
display: flex;
flex-flow: column;
Expand Down
Loading

0 comments on commit 23d407f

Please sign in to comment.