Publish current app and server

This commit is contained in:
ponzischeme89
2026-08-02 22:10:19 +12:00
parent a265636139
commit 1ed180c739
203 changed files with 23933 additions and 2788 deletions
+38 -37
View File
@@ -4,7 +4,7 @@ package main
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -23,6 +23,7 @@ import (
"github.com/ponzischeme89/memby/server/internal/foryou"
"github.com/ponzischeme89/memby/server/internal/library"
"github.com/ponzischeme89/memby/server/internal/logging"
"github.com/ponzischeme89/memby/server/internal/radarr"
"github.com/ponzischeme89/memby/server/internal/recommend"
"github.com/ponzischeme89/memby/server/internal/sonarr"
"github.com/ponzischeme89/memby/server/internal/store"
@@ -43,7 +44,8 @@ func main() {
}
logLevel := logging.ParseLevel(os.Getenv("MEMBY_LOG_LEVEL"))
log, events := logging.NewBuffered(os.Stdout, logLevel, 20_000)
logCapacity := logging.ParseCapacity(os.Getenv("MEMBY_LOG_BUFFER_CAPACITY"), 5_000)
log, events := logging.NewBuffered(os.Stdout, logLevel, logCapacity)
if err := run(log, events); err != nil {
log.Error("fatal", "error", err)
@@ -88,36 +90,21 @@ func run(log *slog.Logger, events *logging.Buffer) error {
}
embyClient := emby.New(cfg.EmbyURL, cfg.EmbyPublicURL, cfg.ClientName, cfg.UpstreamTimeout)
retiredSessions, err := st.TrimSessionsToLimit(ctx, cfg.MaxClientsPerUser)
if err != nil {
return err
}
for _, sess := range retiredSessions {
_ = ca.Delete(ctx, cache.SessionKey(hex.EncodeToString(sess.TokenHash)))
if err := embyClient.Logout(ctx, emby.Credentials{
UserID: sess.EmbyUserID, Token: sess.EmbyToken,
DeviceID: sess.DeviceID, DeviceName: sess.DeviceName,
}); err != nil {
log.Warn("could not retire excess emby session",
"username", sess.Username,
"device", sess.DeviceName,
"error", err,
)
}
}
if len(retiredSessions) > 0 {
log.Info("enforced per-user device allowance",
"retired_sessions", len(retiredSessions),
"max_clients_per_user", cfg.MaxClientsPerUser,
)
}
var sonarrClient *sonarr.Client
if cfg.SonarrURL != "" {
sonarrClient = sonarr.New(cfg.SonarrURL, cfg.SonarrAPIKey, cfg.UpstreamTimeout)
log.Info("sonarr integration enabled", "url", cfg.SonarrURL)
}
var radarrClient *radarr.Client
if cfg.RadarrURL != "" {
radarrClient = radarr.New(cfg.RadarrURL, cfg.RadarrAPIKey, cfg.UpstreamTimeout)
log.Info("radarr integration enabled", "url", cfg.RadarrURL)
}
recommender := recommend.NewEngine(embyClient, log)
if cfg.RecommendationWeights != "" {
_ = json.Unmarshal([]byte(cfg.RecommendationWeights), &recommender.WeightedConfig)
}
// Candidates come from the imported library when one exists, which keeps the
// recommendation rebuild off Emby entirely.
recommender.Library = st
@@ -145,18 +132,11 @@ func run(log *slog.Logger, events *logging.Buffer) error {
st, tracearrClient, recommender, log,
cfg.ForYouMinRebuildAge, cfg.ForYouRefreshInterval,
)
forYouService.ConfigureTimeContext(cfg.SonarrLocation)
forYouService.ConfigureHouseholdUsers(embyClient, emby.Credentials{
UserID: cfg.SyncUserID, Token: cfg.SyncAPIKey,
DeviceID: "memby-for-you-builder", DeviceName: "Memby For You builder",
})
syncer.SetAfterSync(func() {
go func() {
rebuildCtx, cancel := context.WithTimeout(context.Background(), cfg.RecommendTimeout)
defer cancel()
forYouService.MarkAllDirty(rebuildCtx)
_ = forYouService.RebuildAll(rebuildCtx, false)
}()
})
}
server := api.New(cfg, api.Deps{
@@ -166,15 +146,31 @@ func run(log *slog.Logger, events *logging.Buffer) error {
Recommender: recommender,
ForYou: forYouService,
Sonarr: sonarrClient,
Radarr: radarrClient,
Syncer: syncer,
Log: log,
Events: events,
})
// Installed after the server exists, because both halves of a finished import are
// its business: derived data has to be invalidated, and the TVs told the catalogue
// moved. Kept off the syncer itself so library stays ignorant of the API.
syncer.SetAfterSync(func(result library.Result) {
afterCtx, cancel := context.WithTimeout(context.Background(), cfg.RecommendTimeout)
defer cancel()
server.AnnounceLibrarySync(afterCtx, result)
if forYouService == nil || (result.Changed == 0 && result.Removed == 0) {
return
}
forYouService.MarkAllDirty(afterCtx)
})
if err := server.LoadMaintenance(ctx); err != nil {
return err
}
go server.WatchMaintenance(ctx, 30*time.Second)
// One probe per gateway, not per TV: the answer is the same for the whole house.
go server.WatchEmbyReachability(ctx, cfg.EmbyHealthInterval)
if err := server.LoadUpdatePolicy(ctx); err != nil {
return err
@@ -194,17 +190,22 @@ func run(log *slog.Logger, events *logging.Buffer) error {
ctx,
cfg.TracearrSyncInterval,
cfg.TracearrFullInterval,
cfg.ForYouRefreshInterval,
cfg.ForYouRebuildHour,
)
go func() {
importCtx, cancel := context.WithTimeout(ctx, cfg.SyncTimeout)
defer cancel()
if _, err := forYouService.Import(importCtx, false); err != nil {
cancel()
log.Warn("startup Tracearr import failed", "error", err)
return
}
if err := forYouService.RebuildAll(importCtx, false); err != nil {
log.Warn("startup For You rebuild failed", "error", err)
cancel()
// Only an algorithm-version change warrants startup work. Normal dirty
// profiles wait for the daily off-peak rebuild.
rebuildCtx, rebuildCancel := context.WithTimeout(ctx, cfg.SyncTimeout)
defer rebuildCancel()
if err := forYouService.RebuildOutdated(rebuildCtx); err != nil {
log.Warn("startup outdated For You rebuild failed", "error", err)
}
}()
}