jf-activity: initial changes

functionality mostly there but needs a UI.
This commit is contained in:
Harvey Tindall
2025-12-16 18:15:39 +00:00
parent e4e9369d54
commit c21df253a1
11 changed files with 435 additions and 14 deletions

View File

@@ -162,7 +162,7 @@ $(SWAGGER_TARGET): $(SWAGGER_SRC)
$(SWAGINSTALL) $(SWAGINSTALL)
swag init --parseDependency --parseInternal -g main.go swag init --parseDependency --parseInternal -g main.go
VARIANTS_SRC = $(wildcard html/*.html) VARIANTS_SRC = $(wildcard html/*.html) $(wildcard html/*.txt)
VARIANTS_TARGET = $(DATA)/html/admin.html VARIANTS_TARGET = $(DATA)/html/admin.html
$(VARIANTS_TARGET): $(VARIANTS_SRC) $(VARIANTS_TARGET): $(VARIANTS_SRC)
$(info copying html) $(info copying html)
@@ -184,7 +184,7 @@ CSS_FULLTARGET = $(CSS_BUNDLE)
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC) $(SYNTAX_LIGHT_SRC) $(SYNTAX_DARK_SRC) ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC) $(SYNTAX_LIGHT_SRC) $(SYNTAX_DARK_SRC)
ALL_CSS_TARGET = $(ICON_TARGET) ALL_CSS_TARGET = $(ICON_TARGET)
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html) $(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html) $(wildcard html.*.txt)
$(info copying fonts) $(info copying fonts)
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/ cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
cp -r $(SYNTAX_LIGHT_SRC) $(SYNTAX_LIGHT_TARGET) cp -r $(SYNTAX_LIGHT_SRC) $(SYNTAX_LIGHT_TARGET)

View File

@@ -99,7 +99,7 @@ func (app *appContext) generateActivitiesQuery(req ServerFilterReqDTO) *badgerho
for _, q := range req.Queries { for _, q := range req.Queries {
nq := q.AsDBQuery(query) nq := q.AsDBQuery(query)
if nq == nil { if nq == nil {
nq = ActivityDBQueryFromSpecialField(app.jf, query, q) nq = ActivityDBQueryFromSpecialField(app.jf.MediaBrowser, query, q)
} }
query = nq query = nq
} }
@@ -156,8 +156,8 @@ func (app *appContext) GetActivities(gc *gin.Context) {
Value: act.Value, Value: act.Value,
Time: act.Time.Unix(), Time: act.Time.Unix(),
IP: act.IP, IP: act.IP,
Username: act.MustGetUsername(app.jf), Username: act.MustGetUsername(app.jf.MediaBrowser),
SourceUsername: act.MustGetSourceUsername(app.jf), SourceUsername: act.MustGetSourceUsername(app.jf.MediaBrowser),
} }
if act.Type == ActivityDeletion || act.Type == ActivityCreation { if act.Type == ActivityDeletion || act.Type == ActivityCreation {
// Username would've been in here, clear it to avoid confusion to the consumer // Username would've been in here, clear it to avoid confusion to the consumer

View File

@@ -1418,3 +1418,26 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
app.InvalidateUserCaches() app.InvalidateUserCaches()
gc.JSON(code, errors) gc.JSON(code, errors)
} }
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID. Returns as many as the server has recorded.
// @Produce json
// @Success 200 {object} ActivityLogEntriesDTO
// @Failure 400 {object} boolResponse
// @Param id path string true "id of user to fetch activities of."
// @Router /users/{id}/activities/jellyfin [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetJFActivitesForUser(gc *gin.Context) {
userId := gc.Param("id")
if userId == "" {
respondBool(400, false, gc)
return
}
activities, err := app.jf.activity.ByUserID(userId)
if err != nil {
app.err.Printf(lm.FailedGetJFActivities, err)
respondBool(400, false, gc)
return
}
gc.JSON(200, ActivityLogEntriesDTO{Entries: activities})
}

6
go.mod
View File

@@ -20,7 +20,7 @@ replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
// replace github.com/hrfee/mediabrowser => ../mediabrowser replace github.com/hrfee/mediabrowser => ../mediabrowser
require ( require (
github.com/bwmarrin/discordgo v0.29.0 github.com/bwmarrin/discordgo v0.29.0
@@ -42,10 +42,11 @@ require (
github.com/hrfee/jfa-go/logger v0.0.0-20251123165523-7c9f91711460 github.com/hrfee/jfa-go/logger v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/jfa-go/logmessages v0.0.0-20251123165523-7c9f91711460 github.com/hrfee/jfa-go/logmessages v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/jfa-go/ombi v0.0.0-20251123165523-7c9f91711460 github.com/hrfee/jfa-go/ombi v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/mediabrowser v0.3.33 github.com/hrfee/mediabrowser v0.0.0-00010101000000-000000000000
github.com/hrfee/simple-template v1.1.0 github.com/hrfee/simple-template v1.1.0
github.com/itchyny/timefmt-go v0.1.7 github.com/itchyny/timefmt-go v0.1.7
github.com/lithammer/shortuuid/v3 v3.0.7 github.com/lithammer/shortuuid/v3 v3.0.7
github.com/lutischan-ferenc/systray v1.2.1
github.com/mailgun/mailgun-go/v4 v4.23.0 github.com/mailgun/mailgun-go/v4 v4.23.0
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/robert-nix/ansihtml v1.0.1 github.com/robert-nix/ansihtml v1.0.1
@@ -98,7 +99,6 @@ require (
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lutischan-ferenc/systray v1.2.1 // indirect
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect
github.com/mailgun/errors v0.4.0 // indirect github.com/mailgun/errors v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect

4
go.sum
View File

@@ -194,8 +194,6 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.33 h1:kjUFZc46hNhbOEU4xZNyhGVNjfZ5lENmX95Md1thxiA=
github.com/hrfee/mediabrowser v0.3.33/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/simple-template v1.1.0 h1:PNQDTgc2H0s19/pWuhRh4bncuNJjPrW0fIX77YtY78M= github.com/hrfee/simple-template v1.1.0 h1:PNQDTgc2H0s19/pWuhRh4bncuNJjPrW0fIX77YtY78M=
github.com/hrfee/simple-template v1.1.0/go.mod h1:s9a5QgfqbmT7j9WCC3GD5JuEqvihBEohyr+oYZmr4bA= github.com/hrfee/simple-template v1.1.0/go.mod h1:s9a5QgfqbmT7j9WCC3GD5JuEqvihBEohyr+oYZmr4bA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -459,8 +457,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

246
jf_activity.go Normal file
View File

@@ -0,0 +1,246 @@
package main
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/hrfee/mediabrowser"
)
const (
// ActivityLimit is the maximum number of ActivityLogEntries to fetch at once.
ActivityLimit = 5000
// If ByUserLimitLength is true, ByUserLengthOrBaseLength is the maximum number of records attached
// to a user.
// If false, it is the base amount of entries to allocate for for each user ID, and more will be allocayted as needed.
ByUserLengthOrBaseLength = 128
ByUserLimitLength = false
)
type activityLogEntrySource interface {
GetActivityLog(skip, limit int, since time.Time, hasUserID bool) (mediabrowser.ActivityLog, error)
}
// JFActivityCache is a cache for Jellyfin ActivityLogEntries, intended to be refreshed frequently
// and suited to it by only querying for changes since the last refresh.
type JFActivityCache struct {
jf activityLogEntrySource
cache [ActivityLimit]mediabrowser.ActivityLogEntry
// index into Cache of the entry that should be considered the start (i.e. most recent), and end (i.e. oldest).
start, end int
// Map of activity entry IDs to their index.
byEntryID map[int64]int
// Map of user IDs to a slice of entry indexes they are referenced in, chronologically ordered.
byUserID map[string][]int
LastSync, LastYieldingSync time.Time
// Age of cache before it should be refreshed.
WaitForSyncTimeout time.Duration
syncLock sync.Mutex
syncing bool
// Total number of entries.
Total int
dupesInLastSync int
}
func (c *JFActivityCache) debugString() string {
var b strings.Builder
places := len(strconv.Itoa(ActivityLimit - 1))
b.Grow((ActivityLimit * (places + 1) * 2) + 1)
for i := range c.cache {
fmt.Fprintf(&b, "%0"+strconv.Itoa(places)+"d|", i)
}
b.WriteByte('\n')
for i := range c.cache {
fmt.Fprintf(&b, "%0"+strconv.Itoa(places)+"d|", c.cache[i].ID)
}
return b.String()
}
// NewJFActivityCache returns a Jellyfin ActivityLogEntry cache.
// You should set the timeout low, as events are likely to happen frequently,
// and refreshing should be quick anyway
func NewJFActivityCache(jf activityLogEntrySource, waitForSyncTimeout time.Duration) *JFActivityCache {
c := &JFActivityCache{
jf: jf,
WaitForSyncTimeout: waitForSyncTimeout,
start: -1,
end: -1,
byEntryID: map[int64]int{},
byUserID: map[string][]int{},
Total: 0,
dupesInLastSync: 0,
}
for i := range ActivityLimit {
c.cache[i].ID = -1
}
return c
}
// ByUserID returns a slice of ActivitLogEntries with the given jellyfin ID attached.
func (c *JFActivityCache) ByUserID(jellyfinID string) ([]mediabrowser.ActivityLogEntry, error) {
if err := c.MaybeSync(); err != nil {
return nil, err
}
arr, ok := c.byUserID[jellyfinID]
if !ok {
return nil, nil
}
out := make([]mediabrowser.ActivityLogEntry, len(arr))
for i, aleIdx := range arr {
out[i] = c.cache[aleIdx]
}
return out, nil
}
// ByEntryID returns the ActivityLogEntry with the corresponding ID.
func (c *JFActivityCache) ByEntryID(entryID int64) (entry mediabrowser.ActivityLogEntry, ok bool, err error) {
err = c.MaybeSync()
if err != nil {
return
}
var idx int
idx, ok = c.byEntryID[entryID]
if !ok {
return
}
entry = c.cache[idx]
return
}
// MaybeSync returns once the cache is in a suitable state to read:
// return if cache is fresh, sync if not, or wait if another sync is happening already.
func (c *JFActivityCache) MaybeSync() error {
shouldWaitForSync := time.Now().After(c.LastSync.Add(c.WaitForSyncTimeout))
if !shouldWaitForSync {
return nil
}
syncStatus := make(chan error)
go func(status chan error, c *JFActivityCache) {
c.syncLock.Lock()
alreadySyncing := c.syncing
// We're either already syncing or will be
c.syncing = true
c.syncLock.Unlock()
if !alreadySyncing {
// If we haven't synced, this'll just get max (ActivityLimit),
// If we have, it'll get anything that's happened since then
thisSync := time.Now()
al, err := c.jf.GetActivityLog(-1, ActivityLimit, c.LastYieldingSync, true)
if err != nil {
c.syncLock.Lock()
c.syncing = false
c.syncLock.Unlock()
status <- err
return
}
// Can't trust the source fully, so we need to check for anything we've already got stored
// -before- we decide where the data should go.
recvLength := len(al.Items)
c.dupesInLastSync = 0
for i, ale := range al.Items {
if _, ok := c.byEntryID[ale.ID]; ok {
c.dupesInLastSync = len(al.Items) - i
// If we got the same as before, everything after it we'll also have.
recvLength = i
break
}
}
if recvLength > 0 {
// Lazy strategy: for user ID maps, each refresh we'll rebuild them.
// Wipe them, and then append each new refresh element as we process them.
// Then loop through all the old entries and append them too.
for uid := range c.byUserID {
c.byUserID[uid] = c.byUserID[uid][:0]
}
previousStart := c.start
if c.start == -1 {
c.start = 0
c.end = recvLength - 1
} else {
c.start = ((c.start-recvLength)%ActivityLimit + ActivityLimit) % ActivityLimit
}
if c.cache[c.start].ID != -1 {
c.end = ((c.end-1)%ActivityLimit + ActivityLimit) % ActivityLimit
}
for i := range recvLength {
ale := al.Items[i]
ci := (c.start + i) % ActivityLimit
if c.cache[ci].ID != -1 {
// Since we're overwriting it, remove it from index
delete(c.byEntryID, c.cache[ci].ID)
// don't increment total since we're adding and removing
} else {
c.Total++
}
if ale.UserID != "" {
arr, ok := c.byUserID[ale.UserID]
if !ok {
arr = make([]int, 0, ByUserLengthOrBaseLength)
}
if !ByUserLimitLength || len(arr) < ByUserLengthOrBaseLength {
arr = append(arr, ci)
c.byUserID[ale.UserID] = arr
}
}
c.cache[ci] = ale
c.byEntryID[ale.ID] = ci
}
// If this was the first sync, everything has already been processed in the previous loop.
if previousStart != -1 {
i := previousStart
for {
if c.cache[i].UserID != "" {
arr, ok := c.byUserID[c.cache[i].UserID]
if !ok {
arr = make([]int, 0, ByUserLengthOrBaseLength)
}
if !ByUserLimitLength || len(arr) < ByUserLengthOrBaseLength {
arr = append(arr, i)
c.byUserID[c.cache[i].UserID] = arr
}
}
if i == c.end {
break
}
i = (i + 1) % ActivityLimit
}
}
}
// for i := range c.cache {
// fmt.Printf("%04d|", i)
// }
// fmt.Print("\n")
// for i := range c.cache {
// fmt.Printf("%04d|", c.cache[i].ID)
// }
// fmt.Print("\n")
c.syncLock.Lock()
c.LastSync = thisSync
if recvLength > 0 {
c.LastYieldingSync = thisSync
}
c.syncing = false
c.syncLock.Unlock()
} else {
for c.syncing {
continue
}
}
status <- nil
}(syncStatus, c)
err := <-syncStatus
return err
}

136
jf_activity_test.go Normal file
View File

@@ -0,0 +1,136 @@
package main
import (
"sync"
"testing"
"time"
"github.com/hrfee/mediabrowser"
)
type MockActivityLogSource struct {
logs []mediabrowser.ActivityLogEntry
lock sync.Mutex
i int
}
func (m *MockActivityLogSource) run(size int, delay time.Duration, finished *bool) {
m.logs = make([]mediabrowser.ActivityLogEntry, size)
for i := range len(m.logs) {
m.logs[i].ID = -1
}
m.i = 0
for i := range len(m.logs) {
m.lock.Lock()
log := mediabrowser.ActivityLogEntry{
ID: int64(i),
Date: mediabrowser.Time{time.Now()},
}
m.logs[i] = log
m.i = i + 1
m.lock.Unlock()
time.Sleep(delay)
}
*finished = true
time.Sleep(delay)
}
func (m *MockActivityLogSource) GetActivityLog(skip, limit int, since time.Time, hasUserID bool) (mediabrowser.ActivityLog, error) {
// This may introduce duplicates, but those are handled fine.
// If we don't do this, things go wrong in a way that seems
// very specific to this test setup, and (imo) is not necessarily
// applicable to a real scenario.
// since = since.Add(-time.Millisecond)
out := make([]mediabrowser.ActivityLogEntry, 0, limit)
count := 0
loopCount := 0
m.lock.Lock()
for i := m.i - 1; count < limit && i >= 0; i-- {
loopCount++
if m.logs[i].Date.After(since) {
out = append(out, m.logs[i])
count++
}
}
m.lock.Unlock()
return mediabrowser.ActivityLog{Items: out}, nil
}
func TestJFActivityLog(t *testing.T) {
t.Parallel()
// FIXME: This test is failing
t.Run("Completeness", func(t *testing.T) {
mock := MockActivityLogSource{}
waitForSync := time.Microsecond
cache := NewJFActivityCache(&mock, waitForSync)
finished := false
count := len(cache.cache) - 10
go mock.run(count, time.Millisecond, &finished)
for {
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
if cache.dupesInLastSync > 1 {
t.Logf("got %d dupes in last sync\n", cache.dupesInLastSync)
}
if finished {
// Make sure we got everything
time.Sleep(30 * waitForSync)
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
break
}
}
t.Log(">-\n" + cache.debugString())
if cache.Total != count {
t.Errorf("not all collected: %d < %d", cache.Total, count)
}
})
t.Run("Ordering", func(t *testing.T) {
mock := MockActivityLogSource{}
waitForSync := 5 * time.Millisecond
cache := NewJFActivityCache(&mock, waitForSync)
finished := false
count := len(cache.cache) * 10
go mock.run(count, time.Second/100, &finished)
for {
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
if finished {
// Make sure we got everything
time.Sleep(waitForSync)
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
break
}
}
t.Log(">-\n" + cache.debugString())
i := cache.start
lastID := int64(-1)
t.Logf("cache start=%d, end=%d, total=%d\n", cache.start, cache.end, cache.Total)
for {
if i != cache.start {
if cache.cache[i].ID != lastID-1 {
t.Errorf("next was not previous ID: %d != %d-1 = %d", cache.cache[i].ID, lastID, lastID-1)
return
}
}
lastID = cache.cache[i].ID
if i == cache.end {
break
}
i = (i + 1) % len(cache.cache)
}
})
}

View File

@@ -198,6 +198,8 @@ const (
UserAdminAdjusted = "Admin state for user \"%s\" set to %t" UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\"" UserLabelAdjusted = "Label for user \"%s\" set to \"%s\""
FailedGetJFActivities = "Failed to get ActivityLog entries: %v"
// api.go // api.go
ApplyUpdate = "Applied update" ApplyUpdate = "Applied update"
FailedApplyUpdate = "Failed to apply update: %v" FailedApplyUpdate = "Failed to apply update: %v"

15
main.go
View File

@@ -115,7 +115,10 @@ type appContext struct {
adminUsers []User adminUsers []User
invalidTokens []string invalidTokens []string
// Keeping jf name because I can't think of a better one // Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser jf struct {
*mediabrowser.MediaBrowser
activity *JFActivityCache
}
authJf *mediabrowser.MediaBrowser authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper ombi *OmbiWrapper
js *JellyseerrWrapper js *JellyseerrWrapper
@@ -154,6 +157,14 @@ func generateSecret(length int) (string, error) {
} }
func test(app *appContext) { func test(app *appContext) {
app.jf.activity = NewJFActivityCache(app.jf, 10*time.Second)
for {
c, _ := app.jf.activity.ByUserID("9d8c71d1bac04c4c8e69ce3446c61652")
v, _ := app.jf.GetActivityLog(-1, 1, time.Time{}, true)
fmt.Printf("From the source: %+v\nFrom the cache: %+v\nequal: %t\n", v.Items[0], c[0], v.Items[0].ID == c[0].ID)
time.Sleep(5 * time.Second)
}
fmt.Printf("\n\n----\n\n") fmt.Printf("\n\n----\n\n")
settings := map[string]any{ settings := map[string]any{
"server": app.jf.Server, "server": app.jf.Server,
@@ -429,7 +440,7 @@ func start(asDaemon, firstCall bool) {
app.info.Println(lm.UsingJellyfin) app.info.Println(lm.UsingJellyfin)
} }
app.jf, err = mediabrowser.NewServer( app.jf.MediaBrowser, err = mediabrowser.NewServer(
serverType, serverType,
server, server,
app.config.Section("jellyfin").Key("client").String(), app.config.Section("jellyfin").Key("client").String(),

View File

@@ -2,6 +2,8 @@ package main
import ( import (
"time" "time"
"github.com/hrfee/mediabrowser"
) )
type stringResponse struct { type stringResponse struct {
@@ -513,3 +515,7 @@ type TaskDTO struct {
type LabelsDTO struct { type LabelsDTO struct {
Labels []string `json:'labels"` Labels []string `json:'labels"`
} }
type ActivityLogEntriesDTO struct {
Entries []mediabrowser.ActivityLogEntry `json:"entries"`
}

View File

@@ -206,6 +206,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/user", app.NewUserFromAdmin) api.POST(p+"/user", app.NewUserFromAdmin)
api.POST(p+"/users/extend", app.ExtendExpiry) api.POST(p+"/users/extend", app.ExtendExpiry)
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry) api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
api.GET(p+"/users/:id/activities/jellyfin", app.GetJFActivitesForUser)
api.POST(p+"/users/enable", app.EnableDisableUsers) api.POST(p+"/users/enable", app.EnableDisableUsers)
api.POST(p+"/invites", app.GenerateInvite) api.POST(p+"/invites", app.GenerateInvite)
api.GET(p+"/invites", app.GetInvites) api.GET(p+"/invites", app.GetInvites)