232 lines
5.5 KiB
Go
232 lines
5.5 KiB
Go
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
|
|
}
|