package services import ( "context" "sort" "strings" "github.com/fujin/music-agregator/internal/database" "github.com/fujin/music-agregator/internal/indexer" "github.com/google/uuid" "github.com/rs/zerolog/log" ) type RankedSearchResult struct { indexer.SearchResult Quality string `json:"quality"` Score float64 `json:"score"` } type AlbumSearchResult struct { AlbumID string `json:"album_id"` AlbumTitle string `json:"album_title"` ArtistName string `json:"artist_name"` Results []RankedSearchResult `json:"results"` TotalResults int `json:"total_results"` } func SearchAlbum( ctx context.Context, albumID uuid.UUID, db *database.DB, indexerService *IndexerService, ) (*AlbumSearchResult, error) { log.Info().Str("album_id", albumID.String()).Msg("[ALBUM_SEARCH] starting search") album, err := db.GetAlbumDetailByID(ctx, albumID) if err != nil { log.Error().Err(err).Str("album_id", albumID.String()).Msg("[ALBUM_SEARCH] album not found") return nil, err } log.Info().Str("artist", album.ArtistName).Str("album", album.Title).Msg("[ALBUM_SEARCH] searching for album") var year *uint32 if album.ReleaseDate != nil { y := uint32(album.ReleaseDate.Year()) year = &y } criteria := &indexer.MusicSearchCriteria{ Artist: album.ArtistName, Album: &album.Title, Year: year, Limit: 100, Offset: 0, } results, err := indexerService.Search(ctx, criteria, nil) if err != nil { log.Error().Err(err).Msg("[ALBUM_SEARCH] indexer search failed") return nil, err } log.Info().Int("raw_results", len(results)).Msg("[ALBUM_SEARCH] got raw results from indexers") var rankedResults []RankedSearchResult var blockedCount int for _, r := range results { blocked, _ := db.IsBlocklisted(ctx, r.Title, r.Infohash) if blocked { blockedCount++ continue } quality := detectQuality(r.Title) score := calculateScore(quality, r.Seeders) rankedResults = append(rankedResults, RankedSearchResult{ SearchResult: r, Quality: quality, Score: score, }) } if blockedCount > 0 { log.Info().Int("blocked", blockedCount).Msg("[ALBUM_SEARCH] filtered blocklisted results") } sort.Slice(rankedResults, func(i, j int) bool { return rankedResults[i].Score > rankedResults[j].Score }) if len(rankedResults) > 0 { best := rankedResults[0] seeders := 0 if best.Seeders != nil { seeders = *best.Seeders } log.Info(). Str("title", best.Title). Str("quality", best.Quality). Float64("score", best.Score). Int("seeders", seeders). Msg("[ALBUM_SEARCH] best result") } log.Info().Int("total_results", len(rankedResults)).Msg("[ALBUM_SEARCH] search completed") return &AlbumSearchResult{ AlbumID: albumID.String(), AlbumTitle: album.Title, ArtistName: album.ArtistName, Results: rankedResults, TotalResults: len(rankedResults), }, nil } func detectQuality(title string) string { titleLower := strings.ToLower(title) if strings.Contains(titleLower, "flac") || strings.Contains(titleLower, "lossless") { return "FLAC" } if strings.Contains(titleLower, "24bit") || strings.Contains(titleLower, "24-bit") || strings.Contains(titleLower, "hi-res") { return "FLAC-24bit" } if strings.Contains(titleLower, "320") || strings.Contains(titleLower, "mp3-320") { return "MP3-320" } if strings.Contains(titleLower, "v0") || strings.Contains(titleLower, "vbr") { return "MP3-VBR" } if strings.Contains(titleLower, "mp3") { return "MP3" } if strings.Contains(titleLower, "aac") { return "AAC" } if strings.Contains(titleLower, "ogg") { return "OGG" } return "Unknown" } func calculateScore(quality string, seeders *int) float64 { var score float64 switch quality { case "FLAC-24bit": score = 1000 case "FLAC": score = 900 case "MP3-320": score = 700 case "MP3-VBR": score = 600 case "MP3": score = 500 case "AAC": score = 400 case "OGG": score = 350 default: score = 100 } if seeders != nil && *seeders > 0 { score += float64(*seeders) * 0.1 if *seeders > 100 { score += 50 } } return score } type ArtistSearchResult struct { ArtistID string `json:"artist_id"` ArtistName string `json:"artist_name"` AlbumsSearched int `json:"albums_searched"` Results []AlbumBriefResult `json:"results"` } type AlbumBriefResult struct { AlbumID string `json:"album_id"` AlbumTitle string `json:"album_title"` ResultsCount int `json:"results_count"` } func SearchArtistAlbums( ctx context.Context, foreignArtistID string, db *database.DB, indexerService *IndexerService, ) (*ArtistSearchResult, error) { artist, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID) if err != nil { return nil, err } albums, err := db.GetMonitoredAlbumsByArtist(ctx, artist.ID) if err != nil { return nil, err } var results []AlbumBriefResult for _, album := range albums { searchResult, err := SearchAlbum(ctx, album.ID, db, indexerService) if err != nil { results = append(results, AlbumBriefResult{ AlbumID: album.ID.String(), AlbumTitle: album.Title, ResultsCount: 0, }) continue } results = append(results, AlbumBriefResult{ AlbumID: album.ID.String(), AlbumTitle: album.Title, ResultsCount: searchResult.TotalResults, }) } return &ArtistSearchResult{ ArtistID: foreignArtistID, ArtistName: artist.Name, AlbumsSearched: len(albums), Results: results, }, nil }