first commit

This commit is contained in:
Tomas Dvorak
2026-04-13 17:46:58 +02:00
commit 6e8fedf534
234 changed files with 53808 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/config"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/httpapi"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider/musicbrainz"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider/songlink"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider/spotify"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/provider/webplayer"
"github.com/tdvorak/spotifyrecalg/apps/backend/internal/recommendation"
memstore "github.com/tdvorak/spotifyrecalg/apps/backend/internal/storage/memory"
pgstore "github.com/tdvorak/spotifyrecalg/apps/backend/internal/storage/postgres"
"go.uber.org/zap"
)
func main() {
cfg := config.Load()
logger, err := zap.NewProduction()
if cfg.Environment == "development" {
logger, err = zap.NewDevelopment()
}
if err != nil {
log.Fatalf("create logger: %v", err)
}
defer func() { _ = logger.Sync() }()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
store, cleanup, err := buildStore(ctx, cfg, logger)
if err != nil {
logger.Fatal("initialize storage", zap.Error(err))
}
defer cleanup()
engine := recommendation.NewEngine(recommendation.EngineConfig{
Now: time.Now,
ContentWeight: cfg.ContentWeight,
CollabWeight: cfg.CollaborativeWeight,
PopularityWeight: cfg.PopularityWeight,
ExplorationWeight: cfg.ExplorationWeight,
DiversityLambda: cfg.DiversityLambda,
})
router := httpapi.NewRouter(httpapi.RouterConfig{
Store: store,
Engine: engine,
Provider: buildProviderService(store, cfg),
Logger: logger,
APIKeys: cfg.APIKeys,
Version: cfg.Version,
})
server := &http.Server{
Addr: cfg.HTTPAddr,
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
errCh := make(chan error, 1)
go func() {
logger.Info("api listening", zap.String("addr", cfg.HTTPAddr), zap.String("store", cfg.StoreDriver))
errCh <- server.ListenAndServe()
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
logger.Error("graceful shutdown failed", zap.Error(err))
}
case err := <-errCh:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatal("server stopped", zap.Error(err))
}
}
}
func buildProviderService(store httpapi.Store, cfg config.Config) *provider.Service {
providerStore, ok := store.(provider.Store)
if !ok {
return nil
}
spotifyClient := spotify.New(spotify.Config{
ClientID: cfg.SpotifyClientID,
ClientSecret: cfg.SpotifyClientSecret,
BearerToken: cfg.SpotifyBearerToken,
Market: cfg.SpotifyMarket,
})
webplayerClient := webplayer.NewClient()
songlinkClient := songlink.NewClient()
musicBrainzClient := musicbrainz.New(musicbrainz.Config{
AppName: cfg.MusicBrainzAppName,
Contact: cfg.MusicBrainzContact,
Version: cfg.Version,
})
return provider.NewService(providerStore, spotifyClient, webplayerClient, songlinkClient, musicBrainzClient, provider.ServiceConfig{
DefaultMarket: cfg.SpotifyMarket,
CacheTTL: cfg.ProviderCacheTTL,
Version: cfg.Version,
})
}
func buildStore(ctx context.Context, cfg config.Config, logger *zap.Logger) (httpapi.Store, func(), error) {
if cfg.StoreDriver == "memory" {
store := memstore.New()
if cfg.SeedDemoData {
memstore.SeedLargeCatalog(store)
}
return store, func() {}, nil
}
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
if err != nil {
return nil, nil, err
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, nil, err
}
logger.Info("connected to postgres")
return pgstore.New(pool), pool.Close, nil
}