mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
Merge branch 'jf-log'
accidentally did a fix on the wrong branch, so i'm merging it prematurely.
This commit is contained in:
6
Makefile
6
Makefile
@@ -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)
|
||||||
@@ -248,7 +248,7 @@ $(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
|||||||
$(GOBINARY) mod download
|
$(GOBINARY) mod download
|
||||||
$(info Building)
|
$(info Building)
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
|
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET) $(GOBUILDFLAGS)
|
||||||
|
|
||||||
test: $(BUILDDEPS) $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
test: $(BUILDDEPS) $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
||||||
$(GOBINARY) test -ldflags="$(LDFLAGS)" $(TAGS) -p 1
|
$(GOBINARY) test -ldflags="$(LDFLAGS)" $(TAGS) -p 1
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
19
api-ombi.go
19
api-ombi.go
@@ -12,17 +12,22 @@ import (
|
|||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, error) {
|
// getOmbiUser searches for an ombi user given a Jellyfin user ID. It looks for matching username or matching email address.
|
||||||
|
// If "email"=nil, an email address will be acquired from the DB instead. Passing it manually is useful when changing email address.
|
||||||
|
func (app *appContext) getOmbiUser(jfID string, email *string) (map[string]interface{}, error) {
|
||||||
jfUser, err := app.jf.UserByID(jfID, false)
|
jfUser, err := app.jf.UserByID(jfID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
username := jfUser.Name
|
username := jfUser.Name
|
||||||
email := ""
|
if email == nil {
|
||||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
addr := ""
|
||||||
email = e.Addr
|
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||||
|
addr = e.Addr
|
||||||
|
}
|
||||||
|
email = &addr
|
||||||
}
|
}
|
||||||
user, err := app.ombi.getUser(username, email)
|
user, err := app.ombi.getUser(username, *email)
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OmbiWrapper struct {
|
type OmbiWrapper struct {
|
||||||
OmbiUserByJfID func(jfID string) (map[string]interface{}, error)
|
OmbiUserByJfID func(jfID string, email *string) (map[string]interface{}, error)
|
||||||
*ombiLib.Ombi
|
*ombiLib.Ombi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +196,7 @@ func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) SetContactMethods(jellyfinID string, email *string, discord *DiscordUser, telegram *TelegramUser, contactPrefs *common.ContactPreferences) (err error) {
|
func (ombi *OmbiWrapper) SetContactMethods(jellyfinID string, email *string, discord *DiscordUser, telegram *TelegramUser, contactPrefs *common.ContactPreferences) (err error) {
|
||||||
ombiUser, err := ombi.OmbiUserByJfID(jellyfinID)
|
ombiUser, err := ombi.OmbiUserByJfID(jellyfinID, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,14 +206,14 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
Type: ActivityContactLinked,
|
Type: ActivityContactLinked,
|
||||||
UserID: gc.GetString("jfId"),
|
UserID: id,
|
||||||
SourceType: ActivityUser,
|
SourceType: ActivityUser,
|
||||||
Source: gc.GetString("jfId"),
|
Source: id,
|
||||||
Value: "email",
|
Value: "email",
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}, gc, true)
|
}, gc, true)
|
||||||
|
|
||||||
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
|
app.info.Printf(lm.UserEmailAdjusted, id)
|
||||||
gc.Redirect(http.StatusSeeOther, MustGetNonEmptyURL(PAGES.MyAccount))
|
gc.Redirect(http.StatusSeeOther, MustGetNonEmptyURL(PAGES.MyAccount))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||||
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
|
app.info.Printf(lm.SentConfirmationEmail, id, req.Email)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -716,7 +716,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
func() {
|
func() {
|
||||||
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"))
|
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
||||||
return
|
return
|
||||||
|
|||||||
34
api-users.go
34
api-users.go
@@ -294,6 +294,7 @@ func (app *appContext) PostNewUserFromInvite(nu NewUserData, req ConfirmationKey
|
|||||||
}
|
}
|
||||||
contactPrefs.Email = &(emailStore.Contact)
|
contactPrefs.Email = &(emailStore.Contact)
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
|
// FIXME: Why?
|
||||||
profile.ReferralTemplateKey = profile.ReferralTemplateKey
|
profile.ReferralTemplateKey = profile.ReferralTemplateKey
|
||||||
}
|
}
|
||||||
/// Ensures at least one contact method is enabled.
|
/// Ensures at least one contact method is enabled.
|
||||||
@@ -1369,7 +1370,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
if ombi != nil {
|
if ombi != nil {
|
||||||
errorString := ""
|
errorString := ""
|
||||||
user, err := app.getOmbiUser(id)
|
user, err := app.getOmbiUser(id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorString += fmt.Sprintf("Ombi GetUser: %v ", err)
|
errorString += fmt.Sprintf("Ombi GetUser: %v ", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -1418,3 +1419,34 @@ 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
|
||||||
|
}
|
||||||
|
out := ActivityLogEntriesDTO{
|
||||||
|
Entries: make([]ActivityLogEntryDTO, len(activities)),
|
||||||
|
}
|
||||||
|
for i := range activities {
|
||||||
|
out.Entries[i].ActivityLogEntry = activities[i]
|
||||||
|
out.Entries[i].Date = activities[i].Date.Unix()
|
||||||
|
}
|
||||||
|
app.debug.Printf(lm.GotNEntries, len(activities))
|
||||||
|
gc.JSON(200, out)
|
||||||
|
}
|
||||||
|
|||||||
2
api.go
2
api.go
@@ -201,7 +201,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
ombiUser, err := app.getOmbiUser(user.ID)
|
ombiUser, err := app.getOmbiUser(user.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ func NewConfig(configPathOrContents any, dataPath string, logs LoggerSet) (*Conf
|
|||||||
config.MustSetValue("jellyfin", "cache_timeout", "30")
|
config.MustSetValue("jellyfin", "cache_timeout", "30")
|
||||||
config.MustSetValue("jellyfin", "web_cache_async_timeout", "1")
|
config.MustSetValue("jellyfin", "web_cache_async_timeout", "1")
|
||||||
config.MustSetValue("jellyfin", "web_cache_sync_timeout", "10")
|
config.MustSetValue("jellyfin", "web_cache_sync_timeout", "10")
|
||||||
|
config.MustSetValue("jellyfin", "activity_cache_sync_timeout_seconds", "20")
|
||||||
|
|
||||||
LOGIP = config.Section("advanced").Key("log_ips").MustBool(false)
|
LOGIP = config.Section("advanced").Key("log_ips").MustBool(false)
|
||||||
LOGIPU = config.Section("advanced").Key("log_ips_users").MustBool(false)
|
LOGIPU = config.Section("advanced").Key("log_ips_users").MustBool(false)
|
||||||
|
|||||||
@@ -136,6 +136,13 @@ sections:
|
|||||||
type: number
|
type: number
|
||||||
value: 10
|
value: 10
|
||||||
description: "Synchronise after cache is this old, and wait for it: The accounts tab may take a little longer to load while it does."
|
description: "Synchronise after cache is this old, and wait for it: The accounts tab may take a little longer to load while it does."
|
||||||
|
- setting: activity_cache_sync_timeout
|
||||||
|
name: "Activity cache timeout (minutes)"
|
||||||
|
requires_restart: true
|
||||||
|
advanced: true
|
||||||
|
type: number
|
||||||
|
value: 0.1
|
||||||
|
description: "Synchronise Jellyfin's activity log after cache is this old. It can be pretty low as syncing only pulls new records and so is quick. Note this is unrelated to jfa-go's activity log."
|
||||||
- setting: type
|
- setting: type
|
||||||
name: Server type
|
name: Server type
|
||||||
requires_restart: true
|
requires_restart: true
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||||
|
|||||||
250
jf_activity.go
Normal file
250
jf_activity.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/mediabrowser"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ActivityLimit is the maximum number of ActivityLogEntries to keep in memory.
|
||||||
|
// The array they are stored in is fixed, so (ActivityLimit*unsafe.Sizeof(mediabrowser.ActivityLogEntry))
|
||||||
|
// At writing ActivityLogEntries take up ~160 bytes each, so 1M of memory gives us room for ~6250 records
|
||||||
|
ActivityLimit int = 1e6 / 160
|
||||||
|
// 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 allocated 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 {
|
||||||
|
syncTime := time.Now()
|
||||||
|
shouldWaitForSync := time.Now().After(c.LastSync.Add(c.WaitForSyncTimeout))
|
||||||
|
|
||||||
|
if !shouldWaitForSync {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { fmt.Printf("sync took %v", time.Since(syncTime)) }()
|
||||||
|
|
||||||
|
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: rebuild user ID maps each time.
|
||||||
|
// 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
136
jf_activity_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -331,6 +333,9 @@ const (
|
|||||||
|
|
||||||
// usercache.go
|
// usercache.go
|
||||||
CacheRefreshCompleted = "Usercache refreshed, %d in %.2fs (%f.2u/sec)"
|
CacheRefreshCompleted = "Usercache refreshed, %d in %.2fs (%f.2u/sec)"
|
||||||
|
|
||||||
|
// Other
|
||||||
|
GotNEntries = "got %d entries"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
11
main.go
11
main.go
@@ -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
|
||||||
@@ -429,7 +432,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(),
|
||||||
@@ -442,6 +445,10 @@ func start(asDaemon, firstCall bool) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
|
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
|
||||||
}
|
}
|
||||||
|
app.jf.activity = NewJFActivityCache(
|
||||||
|
app.jf,
|
||||||
|
time.Duration(app.config.Section("jellyfin").Key("activity_cache_sync_timeout_seconds").MustInt(20))*time.Second,
|
||||||
|
)
|
||||||
/*if debugMode {
|
/*if debugMode {
|
||||||
app.jf.Verbose = true
|
app.jf.Verbose = true
|
||||||
}*/
|
}*/
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
|||||||
idList[user.JellyfinID] = vals
|
idList[user.JellyfinID] = vals
|
||||||
}
|
}
|
||||||
for jfID, ids := range idList {
|
for jfID, ids := range idList {
|
||||||
ombiUser, err := app.getOmbiUser(jfID)
|
ombiUser, err := app.getOmbiUser(jfID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
|
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
11
models.go
11
models.go
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stringResponse struct {
|
type stringResponse struct {
|
||||||
@@ -513,3 +515,12 @@ type TaskDTO struct {
|
|||||||
type LabelsDTO struct {
|
type LabelsDTO struct {
|
||||||
Labels []string `json:'labels"`
|
Labels []string `json:'labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivityLogEntriesDTO struct {
|
||||||
|
Entries []ActivityLogEntryDTO `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogEntryDTO struct {
|
||||||
|
mediabrowser.ActivityLogEntry
|
||||||
|
Date int64 `json:"Date"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ enum SelectAllState {
|
|||||||
All = 1,
|
All = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
interface UserDTO {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string | undefined;
|
email: string | undefined;
|
||||||
@@ -177,7 +177,7 @@ const queries = (): { [field: string]: QueryType } => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
class user implements User, SearchableItem {
|
class User implements UserDTO, SearchableItem {
|
||||||
private _id = "";
|
private _id = "";
|
||||||
private _row: HTMLTableRowElement;
|
private _row: HTMLTableRowElement;
|
||||||
private _check: HTMLInputElement;
|
private _check: HTMLInputElement;
|
||||||
@@ -691,7 +691,7 @@ class user implements User, SearchableItem {
|
|||||||
private _checkEvent = () => new CustomEvent("accountCheckEvent", { detail: this.id });
|
private _checkEvent = () => new CustomEvent("accountCheckEvent", { detail: this.id });
|
||||||
private _uncheckEvent = () => new CustomEvent("accountUncheckEvent", { detail: this.id });
|
private _uncheckEvent = () => new CustomEvent("accountUncheckEvent", { detail: this.id });
|
||||||
|
|
||||||
constructor(user: User) {
|
constructor(user: UserDTO) {
|
||||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||||
this._row.classList.add("border-b", "border-dashed", "dark:border-dotted", "dark:border-stone-700");
|
this._row.classList.add("border-b", "border-dashed", "dark:border-dotted", "dark:border-stone-700");
|
||||||
let innerHTML = `
|
let innerHTML = `
|
||||||
@@ -897,7 +897,7 @@ class user implements User, SearchableItem {
|
|||||||
this._row.setAttribute(SearchableItemDataAttribute, v);
|
this._row.setAttribute(SearchableItemDataAttribute, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
update = (user: User) => {
|
update = (user: UserDTO) => {
|
||||||
this.id = user.id;
|
this.id = user.id;
|
||||||
this.name = user.name;
|
this.name = user.name;
|
||||||
this.email = user.email || "";
|
this.email = user.email || "";
|
||||||
@@ -934,7 +934,7 @@ class user implements User, SearchableItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UsersDTO extends paginatedDTO {
|
interface UsersDTO extends paginatedDTO {
|
||||||
users: User[];
|
users: UserDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ExtendExpiryDTO {
|
declare interface ExtendExpiryDTO {
|
||||||
@@ -1000,8 +1000,8 @@ export class accountsList extends PaginatedList {
|
|||||||
private _selectAllState: SelectAllState = SelectAllState.None;
|
private _selectAllState: SelectAllState = SelectAllState.None;
|
||||||
// private _users: { [id: string]: user };
|
// private _users: { [id: string]: user };
|
||||||
// private _ordering: string[] = [];
|
// private _ordering: string[] = [];
|
||||||
get users(): { [id: string]: user } {
|
get users(): { [id: string]: User } {
|
||||||
return this._search.items as { [id: string]: user };
|
return this._search.items as { [id: string]: User };
|
||||||
}
|
}
|
||||||
// set users(v: { [id: string]: user }) { this._search.items = v as SearchableItems; }
|
// set users(v: { [id: string]: user }) { this._search.items = v as SearchableItems; }
|
||||||
|
|
||||||
@@ -1362,7 +1362,7 @@ export class accountsList extends PaginatedList {
|
|||||||
this._columns[headerGetters[i]] = new Column(
|
this._columns[headerGetters[i]] = new Column(
|
||||||
header,
|
header,
|
||||||
headerGetters[i],
|
headerGetters[i],
|
||||||
Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get,
|
Object.getOwnPropertyDescriptor(User.prototype, headerGetters[i]).get,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1501,8 +1501,8 @@ export class accountsList extends PaginatedList {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
add = (u: User) => {
|
add = (u: UserDTO) => {
|
||||||
let domAccount = new user(u);
|
let domAccount = new User(u);
|
||||||
this.users[u.id] = domAccount;
|
this.users[u.id] = domAccount;
|
||||||
// console.log("after appending lengths:", Object.keys(this.users).length, Object.keys(this._search.items).length);
|
// console.log("after appending lengths:", Object.keys(this.users).length, Object.keys(this._search.items).length);
|
||||||
};
|
};
|
||||||
@@ -1991,7 +1991,7 @@ export class accountsList extends PaginatedList {
|
|||||||
sendPWR = () => {
|
sendPWR = () => {
|
||||||
addLoader(this._sendPWR);
|
addLoader(this._sendPWR);
|
||||||
let list = this._collectUsers();
|
let list = this._collectUsers();
|
||||||
let manualUser: user;
|
let manualUser: User;
|
||||||
for (let id of list) {
|
for (let id of list) {
|
||||||
let user = this.users[id];
|
let user = this.users[id];
|
||||||
if (!user.lastNotifyMethod() && !user.email) {
|
if (!user.lastNotifyMethod() && !user.email) {
|
||||||
@@ -2545,7 +2545,7 @@ class Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sorts the user list. previouslyActive is whether this column was previously sorted by, indicating that the direction should change.
|
// Sorts the user list. previouslyActive is whether this column was previously sorted by, indicating that the direction should change.
|
||||||
sort = (users: { [id: string]: user }): string[] => {
|
sort = (users: { [id: string]: User }): string[] => {
|
||||||
let userIDs = Object.keys(users);
|
let userIDs = Object.keys(users);
|
||||||
userIDs.sort((a: string, b: string): number => {
|
userIDs.sort((a: string, b: string): number => {
|
||||||
let av: GetterReturnType = this._getter.call(users[a]);
|
let av: GetterReturnType = this._getter.call(users[a]);
|
||||||
@@ -2560,3 +2560,17 @@ class Column {
|
|||||||
return userIDs;
|
return userIDs;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivitySeverity = "Info" | "Debug" | "Warn" | "Error" | "Fatal";
|
||||||
|
interface ActivityLogEntryDTO {
|
||||||
|
Id: number;
|
||||||
|
Name: string;
|
||||||
|
Overview: string;
|
||||||
|
ShortOverview: string;
|
||||||
|
Type: string;
|
||||||
|
ItemId: string;
|
||||||
|
Date: number;
|
||||||
|
UserId: string;
|
||||||
|
UserPrimaryImageTag: string;
|
||||||
|
Severity: ActivitySeverity;
|
||||||
|
}
|
||||||
|
|||||||
3
users.go
3
users.go
@@ -207,9 +207,10 @@ func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
||||||
|
// FIXME: Add DeleteContactMethod to TPS
|
||||||
if app.ombi != nil {
|
if app.ombi != nil {
|
||||||
var tpUser map[string]any
|
var tpUser map[string]any
|
||||||
tpUser, err = app.getOmbiUser(user.ID)
|
tpUser, err = app.getOmbiUser(user.ID, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if id, ok := tpUser["id"]; ok {
|
if id, ok := tpUser["id"]; ok {
|
||||||
err = app.ombi.DeleteUser(id.(string))
|
err = app.ombi.DeleteUser(id.(string))
|
||||||
|
|||||||
2
views.go
2
views.go
@@ -421,7 +421,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser, err := app.getOmbiUser(jfUser.ID)
|
ombiUser, err := app.getOmbiUser(jfUser.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user