Implement MonitorAlbum: search, parse, filter by quality, add best to qbittorrent

This commit is contained in:
Alexander
2026-05-07 23:21:21 +02:00
parent 79f3f145de
commit 8ad2734964
16 changed files with 1479 additions and 59 deletions
+68 -31
View File
@@ -2,10 +2,12 @@ package indexer
import (
"encoding/xml"
"strconv"
"github.com/rs/zerolog/log"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/release"
"homelab.lan/music-agregator/internal/tracker/rutracker"
)
@@ -42,40 +44,80 @@ type TorznabAttr struct {
Value string `xml:"value,attr"`
}
type SearchItemResult struct {
Title string
DownloadLink string
TorrentPageUrl string
PubDate string
Size int64
Description string
Categories []string
Tracker string
Seeders int
Peers int
Attrs map[string]string
Release *release.Release
}
func (si *SearchItemResult) ToProto() *pb.SearchItem {
var pbAttrs []*pb.TorznabAttr
for k, v := range si.Attrs {
pbAttrs = append(pbAttrs, &pb.TorznabAttr{Name: k, Value: v})
}
return &pb.SearchItem{
Title: si.Title,
DownloadLink: si.DownloadLink,
TorrentPageUrl: si.TorrentPageUrl,
PubDate: si.PubDate,
Size: si.Size,
Description: si.Description,
Categories: si.Categories,
TorznabAttrs: pbAttrs,
Release: si.Release.ToProto(),
}
}
type SearchResponse struct {
Items []*SearchItemResult
}
func (sr *SearchResponse) ToProto() *pb.SearchResponse {
pbItems := make([]*pb.SearchItem, len(sr.Items))
for i, item := range sr.Items {
pbItems[i] = item.ToProto()
}
return &pb.SearchResponse{Result: pbItems}
}
var (
rutrackerParserFactory = rutracker.NewRuTrackerParserFactory()
)
func (sr *SearchResult) ToProto() *pb.SearchResponse {
var pbItems []*pb.SearchItem
var skipped int
func (sr *SearchResult) ToSearchResponse() *SearchResponse {
var items []*SearchItemResult
for _, item := range sr.Items {
release := rutrackerParserFactory.GetParser(item.Categories).Parse(item.Title)
rel := rutrackerParserFactory.GetParser(item.Categories).Parse(item.Title)
log.Trace().
Str("tracker", item.JackettIndexer.ID).
Str("title", item.Title).
Str("artist", release.Artist).
Str("album", release.Album).
Int("year", release.Year).
Bool("parsed", release.ParsedSuccessfully).
Str("artist", rel.Artist).
Str("album", rel.Album).
Int("year", rel.Year).
Bool("parsed", rel.ParsedSuccessfully).
Msg("parsed item")
if !release.ParsedSuccessfully {
skipped++
continue
attrs := make(map[string]string, len(item.TorznabAttrs))
for _, attr := range item.TorznabAttrs {
attrs[attr.Name] = attr.Value
}
pbAttrs := make([]*pb.TorznabAttr, len(item.TorznabAttrs))
for j, attr := range item.TorznabAttrs {
pbAttrs[j] = &pb.TorznabAttr{
Name: attr.Name,
Value: attr.Value,
}
}
seeders, _ := strconv.Atoi(attrs["seeders"])
peers, _ := strconv.Atoi(attrs["peers"])
pbItems = append(pbItems, &pb.SearchItem{
items = append(items, &SearchItemResult{
Title: item.Title,
DownloadLink: item.Link,
TorrentPageUrl: item.Guid,
@@ -83,23 +125,18 @@ func (sr *SearchResult) ToProto() *pb.SearchResponse {
Size: item.Size,
Description: item.Description,
Categories: item.Categories,
Enclosure: &pb.Enclosure{
Url: item.Enclosure.URL,
Length: item.Enclosure.Length,
Type: item.Enclosure.Type,
},
TorznabAttrs: pbAttrs,
Release: release.ToProto(),
Tracker: item.JackettIndexer.ID,
Seeders: seeders,
Peers: peers,
Attrs: attrs,
Release: rel,
})
}
log.Trace().
Int("total", len(sr.Items)).
Int("parsed", len(pbItems)).
Int("skipped", skipped).
Int("items", len(items)).
Msg("conversion complete")
return &pb.SearchResponse{
Result: pbItems,
}
return &SearchResponse{Items: items}
}
+2 -2
View File
@@ -44,10 +44,10 @@ func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest)
log.Debug().
Str("query", req.GetQuery()).
Int("results", len(resp.GetResult())).
Int("results", len(resp.Items)).
Msg("search completed")
return resp, nil
return resp.ToProto(), nil
}
func (server *IndexerServer) Capabilities(ctx context.Context, req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
+3 -4
View File
@@ -34,17 +34,16 @@ func NewIndexerService(cfg config.Config, riverClient *river.Client[pgx.Tx]) (*I
return &IndexerService{indexer: idx}, nil
}
func (service *IndexerService) Search(query string, limit int32, indexer string) (*pb.SearchResponse, error) {
func (service *IndexerService) Search(query string, limit int32, indexer string) (*SearchResponse, error) {
searchResult, err := service.indexer.Search(query, limit, indexer)
if err != nil {
log.Error().Err(err).Msg("failed to search in indexer")
return nil, err
}
log.Trace().Int("raw_items", len(searchResult.Items)).Msg("indexer returned results, converting to proto")
log.Trace().Int("raw_items", len(searchResult.Items)).Msg("indexer returned results")
return searchResult.ToProto(), nil
return searchResult.ToSearchResponse(), nil
}
func (service *IndexerService) Capabilities(req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {