Files
metadata-agregator/docs/research/bedrock-api/analysis/CODEBASE.md
T
Alexander a1f6701bac feat: initial implementation of metadata aggregator
- gRPC service with MusicBrainz provider
- PostgreSQL schema with migrations
- Service layer with database-first caching
- Repository pattern for data access
- YAML configuration support
- Research documentation for 17 music metadata projects
2026-04-28 16:28:53 +02:00

32 KiB

Bedrock-API Codebase Analysis

Project Structure

bedrock-api/
├── bedrock_server/          # Main application
│   ├── main.go              # Service implementation (1329 lines)
│   ├── resolver.go          # Stream resolution logic
│   ├── proxy.go             # HTTP streaming proxy
│   ├── auth.go              # JWT authentication
│   ├── lrclib.go            # Synced lyrics (LrcLib)
│   └── genius.go            # Plain lyrics (Genius)
├── providers/               # Platform adapters
│   ├── spotify.go           # Spotify integration
│   ├── soundcloud.go        # SoundCloud integration
│   ├── deezer.go            # Deezer integration
│   ├── youtube.go           # YouTube Music integration
│   ├── yandex.go            # Yandex stub
│   └── vk.go                # VK stub
├── store/                   # Data access layer
│   └── user.go              # User CRUD operations
├── db/                      # Database
│   └── migrations/          # SQL migration files
├── proto/                   # Protocol buffers
│   └── bedrock_service.proto # gRPC service definition (622 lines)
├── tests/                   # Integration tests
│   ├── auth_test.go
│   ├── spotify_test.go
│   ├── soundcloud_test.go
│   ├── youtube_test.go
│   ├── deezer_test.go
│   └── lyrics_test.go
├── spotapi-go/              # Git submodule (Spotify wrapper)
├── .github/
│   └── workflows/
│       ├── test.yml         # Integration tests
│       └── lint.yml         # Code linting
├── Dockerfile               # Multi-stage build
├── docker-compose.yml       # PostgreSQL only
├── go.mod                   # Go 1.25
├── go.sum
├── .env.example             # Environment template
└── README.md

Total Lines of Code: ~5000+ (excluding tests, proto, submodules)

Configuration Management

Environment Variables

Loading Strategy: Three-location search

File: bedrock_server/main.go

func loadEnv() {
    locations := []string{
        ".env",                    // Current directory
        "bedrock_server/.env",     // Server directory
        "../.env",                 // Parent directory
    }
    
    for _, loc := range locations {
        if err := godotenv.Load(loc); err == nil {
            log.Printf("Loaded environment from %s", loc)
            return
        }
    }
    
    log.Println("No .env file found, using system environment variables")
}

Precedence: First found file wins (no merging)

Required Variables

DATABASE_URL=postgresql://user:pass@host:port/database
JWT_SECRET=your-secret-key

Optional Variables (Provider Credentials)

SPOTIFY_CLIENT_ID=your_id
SPOTIFY_CLIENT_SECRET=your_secret
SOUNDCLOUD_CLIENT_IDS=id1,id2,id3
DEEZER_APP_ID=your_id
YOUTUBE_COOKIES=cookie-string
GENIUS_ACCESS_TOKEN=your_token

CLI Flags

File: bedrock_server/main.go

var (
    grpcPort   = flag.Int("port", 50052, "gRPC server port")
    proxyAddr  = flag.String("proxy-addr", ":8080", "HTTP proxy address")
    proxyHost  = flag.String("proxy-host", "", "HTTP proxy host for URL generation")
)

func main() {
    flag.Parse()
    loadEnv()
    
    // Flags override environment variables
    if *grpcPort != 50052 {
        log.Printf("Using custom gRPC port: %d", *grpcPort)
    }
}

Usage:

./bedrock-server -port 9090 -proxy-addr :8888 -proxy-host https://api.example.com

Configuration Validation

No Validation: Application crashes if required variables are missing

Example Crash:

dbURL := os.Getenv("DATABASE_URL")
pool, err := pgxpool.New(context.Background(), dbURL) // Panics if dbURL is empty

Recommendation: Add startup validation

func validateConfig() error {
    required := []string{"DATABASE_URL", "JWT_SECRET"}
    
    for _, key := range required {
        if os.Getenv(key) == "" {
            return fmt.Errorf("required environment variable %s not set", key)
        }
    }
    
    return nil
}

Logging

Implementation

Library: Go stdlib log package
Format: Plain text with provider prefixes

Examples:

log.Printf("[spotify] Searching for: %s", query)
log.Printf("[soundcloud] Client ID rotation: %d -> %d", old, new)
log.Printf("[youtube] Client %s failed: %v", clientName, err)
log.Printf("[auth] User registered: %s", email)

Log Levels

No Levels: All logs are info-level (no debug/warn/error distinction)

Example (no level):

log.Printf("[spotify] Search failed: %v", err) // Is this error or warning?

Recommendation: Use structured logging with levels

import "go.uber.org/zap"

logger.Info("search request", zap.String("provider", "spotify"), zap.String("query", query))
logger.Error("search failed", zap.String("provider", "spotify"), zap.Error(err))

Log Output

Destination: stdout (default)
Rotation: No (relies on systemd or Docker log rotation)
Aggregation: No (manual collection required)

Systemd Logging:

journalctl -u bedrock-api -f

Docker Logging:

docker logs -f bedrock-api

Correlation IDs

Not Implemented: No request tracing across logs

Recommendation: Add correlation IDs

func (s *server) SearchTracks(ctx context.Context, req *pb.SearchRequest) (*pb.SearchTracksResponse, error) {
    correlationID := uuid.New().String()
    ctx = context.WithValue(ctx, "correlation_id", correlationID)
    
    log.Printf("[%s] Search request: %s", correlationID, req.Query)
    // Pass ctx to providers
}

Authentication Implementation

JWT Token Generation

File: bedrock_server/auth.go

func (s *server) generateTokens(userID, email string) (accessToken, refreshToken string, err error) {
    // Access token (15 minutes)
    accessClaims := jwt.MapClaims{
        "user_id": userID,
        "email":   email,
        "exp":     time.Now().Add(15 * time.Minute).Unix(),
        "iat":     time.Now().Unix(),
    }
    
    accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    accessToken, err = accessTokenObj.SignedString(s.jwtSecret)
    if err != nil {
        return "", "", fmt.Errorf("sign access token: %w", err)
    }
    
    // Refresh token (7 days)
    refreshClaims := jwt.MapClaims{
        "user_id": userID,
        "email":   email,
        "exp":     time.Now().Add(7 * 24 * time.Hour).Unix(),
        "iat":     time.Now().Unix(),
    }
    
    refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refreshToken, err = refreshTokenObj.SignedString(s.jwtSecret)
    if err != nil {
        return "", "", fmt.Errorf("sign refresh token: %w", err)
    }
    
    return accessToken, refreshToken, nil
}

Algorithm: HS256 (HMAC with SHA-256)
Secret: Single shared secret from JWT_SECRET environment variable

Security Considerations:

  • HS256 is symmetric (same key for signing and verification)
  • No key rotation (single secret for all tokens)
  • No token revocation (valid until expiration)

Recommendation: Use RS256 (asymmetric) for better security

// Generate RSA key pair
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey

// Sign with private key
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
tokenString, _ := token.SignedString(privateKey)

// Verify with public key (can be distributed to other services)
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    return publicKey, nil
})

Password Hashing

File: bedrock_server/auth.go

func hashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
    return string(bytes), err
}

func checkPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

Algorithm: bcrypt
Cost Factor: 10 (2^10 = 1024 iterations)
Time: ~100ms per hash (intentionally slow)

Security: Strong (salted, slow, resistant to brute force)

gRPC Interceptors

Unary Interceptor (single request/response):

func (s *server) authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    // Public methods bypass auth
    publicMethods := map[string]bool{
        "/bedrock.BedrockService/Register":        true,
        "/bedrock.BedrockService/Login":           true,
        "/bedrock.BedrockService/RefreshToken":    true,
        "/bedrock.BedrockService/GetServiceStatus": true,
    }
    
    if publicMethods[info.FullMethod] {
        return handler(ctx, req)
    }
    
    // Extract token from metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "missing metadata")
    }
    
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing authorization header")
    }
    
    // Remove "Bearer " prefix
    tokenString := strings.TrimPrefix(tokens[0], "Bearer ")
    
    // Validate token
    claims := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
        // Verify signing method
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return s.jwtSecret, nil
    })
    
    if err != nil || !token.Valid {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }
    
    // Add user info to context
    ctx = context.WithValue(ctx, "user_id", claims["user_id"])
    ctx = context.WithValue(ctx, "email", claims["email"])
    
    return handler(ctx, req)
}

Stream Interceptor (streaming requests):

func (s *server) streamAuthInterceptor(
    srv interface{},
    ss grpc.ServerStream,
    info *grpc.StreamServerInfo,
    handler grpc.StreamHandler,
) error {
    // Similar logic to unary interceptor
    // Validates token once at stream start
    // No per-message validation
}

Registration:

grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(s.authInterceptor),
    grpc.StreamInterceptor(s.streamAuthInterceptor),
)

Registration Flow

File: bedrock_server/main.go

func (s *server) Register(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) {
    // Validate email format
    if !isValidEmail(req.Email) {
        return nil, status.Error(codes.InvalidArgument, "invalid email format")
    }
    
    // Hash password
    passwordHash, err := hashPassword(req.Password)
    if err != nil {
        return nil, status.Error(codes.Internal, "failed to hash password")
    }
    
    // Save user
    userStore := store.NewUserStore(s.db)
    userID, err := userStore.Save(ctx, req.Email, passwordHash)
    if err != nil {
        if strings.Contains(err.Error(), "duplicate key") {
            return nil, status.Error(codes.AlreadyExists, "email already registered")
        }
        return nil, status.Error(codes.Internal, "failed to create user")
    }
    
    // Generate tokens
    accessToken, refreshToken, err := s.generateTokens(userID, req.Email)
    if err != nil {
        return nil, status.Error(codes.Internal, "failed to generate tokens")
    }
    
    return &pb.AuthResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        User: &pb.User{
            Id:         userID,
            Email:      req.Email,
            Role:       "user",
            IsVerified: false,
        },
    }, nil
}

No Password Requirements: Any password is accepted (no minimum length, complexity rules)

Recommendation: Add password validation

func validatePassword(password string) error {
    if len(password) < 8 {
        return errors.New("password must be at least 8 characters")
    }
    
    hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
    hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
    hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
    
    if !hasUpper || !hasLower || !hasDigit {
        return errors.New("password must contain uppercase, lowercase, and digit")
    }
    
    return nil
}

Login Flow

func (s *server) Login(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) {
    // Find user by email
    userStore := store.NewUserStore(s.db)
    user, err := userStore.Find(ctx, req.Email)
    if err != nil {
        return nil, status.Error(codes.NotFound, "user not found")
    }
    
    // Verify password
    if !checkPasswordHash(req.Password, user.PasswordHash) {
        return nil, status.Error(codes.Unauthenticated, "invalid credentials")
    }
    
    // Generate tokens
    accessToken, refreshToken, err := s.generateTokens(user.ID, user.Email)
    if err != nil {
        return nil, status.Error(codes.Internal, "failed to generate tokens")
    }
    
    return &pb.AuthResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        User: &pb.User{
            Id:         user.ID,
            Email:      user.Email,
            Role:       user.Role,
            IsVerified: user.IsVerified,
        },
    }, nil
}

No Rate Limiting: Unlimited login attempts (brute force possible)

Recommendation: Add rate limiting

import "golang.org/x/time/rate"

var loginLimiters = make(map[string]*rate.Limiter)
var mu sync.Mutex

func getLoginLimiter(email string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    
    limiter, exists := loginLimiters[email]
    if !exists {
        limiter = rate.NewLimiter(rate.Every(time.Minute), 5) // 5 attempts per minute
        loginLimiters[email] = limiter
    }
    
    return limiter
}

func (s *server) Login(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) {
    limiter := getLoginLimiter(req.Email)
    if !limiter.Allow() {
        return nil, status.Error(codes.ResourceExhausted, "too many login attempts")
    }
    
    // Continue with login logic
}

Testing

Test Structure

Directory: tests/

Files:

  • auth_test.go - Authentication tests (register, login, refresh)
  • spotify_test.go - Spotify provider tests
  • soundcloud_test.go - SoundCloud provider tests
  • youtube_test.go - YouTube Music provider tests
  • deezer_test.go - Deezer provider tests
  • lyrics_test.go - Lyrics integration tests (LrcLib, Genius)

Integration Tests

Example: tests/spotify_test.go

func TestSpotifySearch(t *testing.T) {
    // Connect to test server
    addr := os.Getenv("BEDROCK_TEST_ADDR")
    if addr == "" {
        addr = "localhost:50052"
    }
    
    conn, err := grpc.Dial(addr, grpc.WithInsecure())
    if err != nil {
        t.Fatalf("dial: %v", err)
    }
    defer conn.Close()
    
    client := pb.NewBedrockServiceClient(conn)
    
    // Register test user
    authResp, err := client.Register(context.Background(), &pb.AuthRequest{
        Email:    "test@example.com",
        Password: "password123",
    })
    if err != nil {
        t.Fatalf("register: %v", err)
    }
    
    // Authenticated context
    ctx := metadata.AppendToOutgoingContext(
        context.Background(),
        "authorization", "Bearer "+authResp.AccessToken,
    )
    
    // Search tracks
    resp, err := client.SearchTracks(ctx, &pb.SearchRequest{
        Query: "Bohemian Rhapsody",
        Limit: 10,
    })
    if err != nil {
        t.Fatalf("search: %v", err)
    }
    
    // Verify results
    if len(resp.Tracks) == 0 {
        t.Fatal("no tracks returned")
    }
    
    // Verify Spotify results present
    hasSpotify := false
    for _, track := range resp.Tracks {
        if track.Platform == pb.Platform_SPOTIFY {
            hasSpotify = true
            break
        }
    }
    
    if !hasSpotify {
        t.Error("no Spotify results found")
    }
}

Test Requirements:

  • Running server (BEDROCK_TEST_ADDR)
  • PostgreSQL database
  • Provider credentials (environment variables)

Test Timeout: 120 seconds (configured in GitHub Actions)

No Unit Tests

Missing:

  • Provider adapter unit tests (mocked HTTP responses)
  • Database store unit tests (mocked database)
  • Authentication unit tests (mocked JWT)
  • Stream resolution unit tests

Recommendation: Add unit tests with mocks

func TestSpotifyProvider_SearchTracks(t *testing.T) {
    // Mock HTTP server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{
            "tracks": {
                "items": [
                    {
                        "id": "abc123",
                        "name": "Test Track",
                        "artists": [{"id": "artist1", "name": "Test Artist"}],
                        "album": {"id": "album1", "name": "Test Album"}
                    }
                ]
            }
        }`))
    }))
    defer server.Close()
    
    // Create provider with mock server URL
    provider := &SpotifyProvider{
        client: spotify.New(/* mock client */),
    }
    
    // Test search
    tracks, err := provider.SearchTracks(context.Background(), "test", 10)
    if err != nil {
        t.Fatalf("search failed: %v", err)
    }
    
    if len(tracks) != 1 {
        t.Errorf("expected 1 track, got %d", len(tracks))
    }
}

Test Coverage

No Coverage Reports: Coverage not measured

Recommendation: Add coverage reporting

go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

GitHub Actions Integration:

- name: Run tests with coverage
  run: go test -v -coverprofile=coverage.out ./...

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage.out

Health Checks

Service Status

File: bedrock_server/main.go

func (s *server) GetServiceStatus(ctx context.Context, req *pb.Empty) (*pb.ServiceStatusResponse, error) {
    // Stub implementation (always returns healthy)
    return &pb.ServiceStatusResponse{
        Status: pb.ServiceStatus_HEALTHY,
        Dependencies: []*pb.DependencyStatus{
            {Name: "spotify", Health: pb.HealthStatus_HEALTHY, Latency: 0},
            {Name: "soundcloud", Health: pb.HealthStatus_HEALTHY, Latency: 0},
            {Name: "deezer", Health: pb.HealthStatus_HEALTHY, Latency: 0},
            {Name: "youtube", Health: pb.HealthStatus_HEALTHY, Latency: 0},
            {Name: "postgres", Health: pb.HealthStatus_HEALTHY, Latency: 0},
        },
    }, nil
}

Issues:

  • No actual health checks (stub only)
  • No latency measurement
  • No database connection check
  • No provider API checks

Recommendation: Implement real health checks

func (s *server) GetServiceStatus(ctx context.Context, req *pb.Empty) (*pb.ServiceStatusResponse, error) {
    var dependencies []*pb.DependencyStatus
    
    // Check database
    dbStart := time.Now()
    if err := s.db.Ping(ctx); err != nil {
        dependencies = append(dependencies, &pb.DependencyStatus{
            Name:    "postgres",
            Health:  pb.HealthStatus_UNHEALTHY,
            Latency: 0,
        })
    } else {
        dependencies = append(dependencies, &pb.DependencyStatus{
            Name:    "postgres",
            Health:  pb.HealthStatus_HEALTHY,
            Latency: int32(time.Since(dbStart).Milliseconds()),
        })
    }
    
    // Check each provider
    for _, provider := range s.providers {
        providerStart := time.Now()
        _, err := provider.SearchTracks(ctx, "test", 1)
        
        health := pb.HealthStatus_HEALTHY
        if err != nil {
            health = pb.HealthStatus_UNHEALTHY
        }
        
        dependencies = append(dependencies, &pb.DependencyStatus{
            Name:    provider.Name(),
            Health:  health,
            Latency: int32(time.Since(providerStart).Milliseconds()),
        })
    }
    
    // Determine overall status
    status := pb.ServiceStatus_HEALTHY
    for _, dep := range dependencies {
        if dep.Health == pb.HealthStatus_UNHEALTHY {
            status = pb.ServiceStatus_DEGRADED
            break
        }
    }
    
    return &pb.ServiceStatusResponse{
        Status:       status,
        Dependencies: dependencies,
    }, nil
}

Readiness vs Liveness

Not Implemented: No distinction between readiness and liveness

Kubernetes Probes (recommended):

livenessProbe:
  exec:
    command:
    - grpc_health_probe
    - -addr=:50052
  initialDelaySeconds: 10
  periodSeconds: 10

readinessProbe:
  exec:
    command:
    - grpc_health_probe
    - -addr=:50052
    - -service=bedrock.BedrockService
  initialDelaySeconds: 5
  periodSeconds: 5

gRPC Health Checking Protocol:

import "google.golang.org/grpc/health/grpc_health_v1"

type healthServer struct {
    grpc_health_v1.UnimplementedHealthServer
}

func (h *healthServer) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
    // Check if service is ready
    return &grpc_health_v1.HealthCheckResponse{
        Status: grpc_health_v1.HealthCheckResponse_SERVING,
    }, nil
}

// Register health server
grpc_health_v1.RegisterHealthServer(grpcServer, &healthServer{})

Error Handling

Error Patterns

Provider Errors: Log and continue

tracks, err := provider.SearchTracks(ctx, query, limit)
if err != nil {
    log.Printf("[%s] Search failed: %v", provider.Name(), err)
    errors = append(errors, &pb.ProviderError{
        Provider: provider.Name(),
        Message:  err.Error(),
    })
    // Don't return, continue to next provider
}

Database Errors: Return immediately

user, err := userStore.Find(ctx, email)
if err != nil {
    return nil, status.Error(codes.NotFound, "user not found")
}

gRPC Status Codes:

Code Usage
OK Successful operation
InvalidArgument Invalid request parameters
NotFound Entity not found
AlreadyExists Duplicate entity (email)
Unauthenticated Missing or invalid JWT
Internal Server error
ResourceExhausted Rate limit (not implemented)

Error Wrapping

No Error Wrapping: Errors are not wrapped with context

Example (no wrapping):

if err != nil {
    return nil, err
}

Recommendation: Wrap errors with context

if err != nil {
    return nil, fmt.Errorf("search spotify: %w", err)
}

Benefits:

  • Error chain for debugging
  • Context preservation
  • Stack trace (with errors package)

Code Style

Comment Linting

Custom Linter: .github/workflows/lint.yml

Rules:

  1. No decorative comments (// ========, // --------, etc.)
  2. No uppercase-leading comments (except TODO, FIXME, NOTE)

Examples:

Forbidden:

// ========================================
// Spotify Provider
// ========================================

// This function searches for tracks
func SearchTracks() {}

Allowed:

// searchTracks queries Spotify API for tracks matching the query
func searchTracks() {}

// TODO: Add caching
func searchTracks() {}

Enforcement: GitHub Actions fails on violations

Naming Conventions

Exported Functions: PascalCase

func SearchTracks() {}
func GetStreamURL() {}

Unexported Functions: camelCase

func parseNamespacedID() {}
func selectBestFormat() {}

Constants: PascalCase or SCREAMING_SNAKE_CASE

const DefaultLimit = 20
const MAX_RETRIES = 3

Interfaces: Noun or adjective ending in "er"

type trackProvider interface {}
type streamResolver interface {}

Code Organization

Single File Service: main.go (1329 lines)

Issues:

  • All RPC methods in one file
  • Hard to navigate
  • Merge conflicts likely

Recommendation: Split by domain

bedrock_server/
├── main.go              # Server setup, initialization
├── search.go            # Search methods
├── retrieval.go         # Get methods
├── streaming.go         # Stream methods
├── recommendations.go   # Similar tracks
├── statistics.go        # Top tracks/albums/artists
├── import.go            # Playlist import
├── auth.go              # Authentication
└── lyrics.go            # Lyrics methods

Dependency Management

Go Modules

File: go.mod

module github.com/feralbureau/bedrock-api

go 1.25

require (
    github.com/golang-jwt/jwt/v5 v5.2.1
    github.com/jackc/pgx/v5 v5.7.2
    github.com/joho/godotenv v1.5.1
    github.com/kkdai/youtube/v2 v2.10.3
    github.com/rhnvrm/lyric-api-go v0.1.4
    golang.org/x/crypto v0.31.0
    google.golang.org/grpc v1.79.1
    google.golang.org/protobuf v1.36.4
)

Direct Dependencies: 8
Indirect Dependencies: ~50 (transitive)

Submodule Dependency

Submodule: spotapi-go (custom Spotify wrapper)

Issues:

  • Custom fork (not official library)
  • Maintenance burden
  • Submodule initialization required

Recommendation: Use official library directly

import "github.com/zmb3/spotify/v2"

// Remove spotapi-go submodule
// Use spotify/v2 directly

Dependency Updates

No Automated Updates: Dependabot not configured

Recommendation: Add Dependabot

File: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Performance Considerations

Goroutine Management

Unbounded Goroutines: No limit on concurrent goroutines

Example:

for _, provider := range providers {
    wg.Add(1)
    go func(p trackProvider) {
        defer wg.Done()
        // Query provider
    }(provider)
}

Risk: High request volume spawns thousands of goroutines

Recommendation: Use worker pool

type workerPool struct {
    workers int
    tasks   chan func()
}

func newWorkerPool(workers int) *workerPool {
    p := &workerPool{
        workers: workers,
        tasks:   make(chan func(), workers*2),
    }
    
    for i := 0; i < workers; i++ {
        go p.worker()
    }
    
    return p
}

func (p *workerPool) worker() {
    for task := range p.tasks {
        task()
    }
}

func (p *workerPool) submit(task func()) {
    p.tasks <- task
}

Connection Pooling

HTTP Clients: Reused per provider (good)

type SoundCloudProvider struct {
    httpClient *http.Client
}

func NewSoundCloudProvider() *SoundCloudProvider {
    return &SoundCloudProvider{
        httpClient: &http.Client{
            Timeout: 10 * time.Second,
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
            },
        },
    }
}

Database: Connection pooling configured (good)

config.MaxConns = 10
config.MinConns = 2

Memory Allocation

No Object Pooling: Objects allocated per request

Recommendation: Use sync.Pool for frequently allocated objects

var trackPool = sync.Pool{
    New: func() interface{} {
        return &pb.Track{}
    },
}

func getTrack() *pb.Track {
    return trackPool.Get().(*pb.Track)
}

func putTrack(t *pb.Track) {
    // Reset fields
    t.Id = ""
    t.Title = ""
    // ...
    trackPool.Put(t)
}

Security Best Practices

Input Validation

Minimal Validation: Only email format checked

Missing:

  • Query length limits (SQL injection via search)
  • ID format validation
  • Limit parameter bounds

Recommendation: Add comprehensive validation

func validateSearchRequest(req *pb.SearchRequest) error {
    if len(req.Query) == 0 {
        return errors.New("query cannot be empty")
    }
    
    if len(req.Query) > 500 {
        return errors.New("query too long (max 500 characters)")
    }
    
    if req.Limit < 1 || req.Limit > 50 {
        return errors.New("limit must be between 1 and 50")
    }
    
    return nil
}

SQL Injection Prevention

Parameterized Queries: All queries use placeholders (good)

err := s.db.QueryRow(ctx,
    "SELECT * FROM users WHERE email = $1",
    email,
).Scan(&user)

No String Concatenation: No SQL injection risk

Secrets Management

Environment Variables: Secrets in plaintext .env files

Recommendation: Use secrets manager

import "github.com/aws/aws-sdk-go/service/secretsmanager"

func getSecret(secretName string) (string, error) {
    svc := secretsmanager.New(session.New())
    
    result, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{
        SecretId: aws.String(secretName),
    })
    
    if err != nil {
        return "", err
    }
    
    return *result.SecretString, nil
}

Code Quality Metrics

Cyclomatic Complexity

High Complexity: main.go (1329 lines, 23 methods)

Recommendation: Split into smaller files

Code Duplication

Provider Adapters: Similar patterns across providers (acceptable)

Search Methods: Identical fan-out pattern (could be abstracted)

Recommendation: Extract common fan-out logic

func (s *server) fanOutSearch(
    ctx context.Context,
    providers []trackProvider,
    searchFunc func(trackProvider) ([]*pb.Track, error),
) ([]*pb.Track, []*pb.ProviderError, pb.ResponseStatus) {
    var (
        mu      sync.Mutex
        wg      sync.WaitGroup
        tracks  []*pb.Track
        errors  []*pb.ProviderError
    )
    
    for _, provider := range providers {
        wg.Add(1)
        go func(p trackProvider) {
            defer wg.Done()
            
            results, err := searchFunc(p)
            
            mu.Lock()
            defer mu.Unlock()
            
            if err != nil {
                errors = append(errors, &pb.ProviderError{
                    Provider: p.Name(),
                    Message:  err.Error(),
                })
            } else {
                tracks = append(tracks, results...)
            }
        }(provider)
    }
    
    wg.Wait()
    
    status := pb.ResponseStatus_OK
    if len(errors) > 0 {
        if len(tracks) == 0 {
            status = pb.ResponseStatus_ERROR
        } else {
            status = pb.ResponseStatus_PARTIAL
        }
    }
    
    return tracks, errors, status
}

Documentation

No Package Documentation: Missing package-level comments

Recommendation: Add package docs

// Package bedrock provides a unified music metadata and streaming API
// that aggregates data from multiple music platforms (Spotify, SoundCloud,
// Deezer, YouTube Music).
//
// The service exposes a gRPC interface with 23 methods for searching,
// retrieving, and streaming music content. It also provides an HTTP proxy
// for streaming audio and album art.
//
// Authentication is handled via JWT tokens with bcrypt password hashing.
// All provider queries are executed in parallel for optimal performance.
package main

Recommendations for Metadata Aggregator

Adopt

  • Provider interface pattern (clean abstraction)
  • Fan-out concurrency (parallel queries)
  • Partial response handling (resilient to failures)
  • gRPC interceptors (authentication)
  • bcrypt password hashing (secure)
  • Parameterized queries (SQL injection safe)

Avoid

  • Single 1300+ line file (split by domain)
  • No unit tests (add mocked tests)
  • No error wrapping (add context)
  • Unbounded goroutines (use worker pool)
  • No input validation (validate all inputs)
  • Stub health checks (implement real checks)

Enhance

  • Add structured logging (zap, zerolog)
  • Add metrics (Prometheus)
  • Add caching (Redis)
  • Add rate limiting (per-user, per-provider)
  • Add circuit breakers (failing providers)
  • Add retry logic (exponential backoff)
  • Add comprehensive validation
  • Add unit tests with mocks
  • Add code coverage reporting
  • Add API documentation (OpenAPI/Swagger for HTTP, gRPC reflection)