Add Jacket indexer with capabilities implemented

This commit is contained in:
Alexander
2026-05-04 18:40:31 +02:00
parent 32eb8c931e
commit 8ffa92276e
13 changed files with 375 additions and 14 deletions
+24
View File
@@ -0,0 +1,24 @@
meta {
name: Capabilities
type: grpc
seq: 1
}
grpc {
url: localhost:3000
method: /music_agregator.indexer.v1.IndexerService/Capabilities
body: grpc
protoPath: ../proto/music_agregator/indexer/v1/indexer.proto
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"indexer": "rutracker"
}
'''
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Indexer
seq: 7
}
auth {
mode: inherit
}
+20
View File
@@ -0,0 +1,20 @@
meta {
name: Capabilities
type: http
seq: 2
}
get {
url: http://localhost:9117/api/v2.0/indexers/rutracker/results/torznab/api?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&t=caps
body: none
auth: inherit
}
params:query {
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
t: caps
}
settings {
encodeUrl: true
}
+23
View File
@@ -0,0 +1,23 @@
meta {
name: Search
type: http
seq: 1
}
get {
url: http://localhost:9117/api/v2.0/indexers/all/results/torznab?apikey=3jfvdvt1etzz36drkw5id5sb95sc47fi&limit=1&artist=Metallica&t=music
body: none
auth: inherit
}
params:query {
apikey: 3jfvdvt1etzz36drkw5id5sb95sc47fi
limit: 1
artist: Metallica
t: music
}
settings {
encodeUrl: true
timeout: 0
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Jackett
seq: 5
}
auth {
mode: inherit
}
+4
View File
@@ -13,6 +13,10 @@
{
"path": "../proto/music_agregator/hello/v1/service.proto",
"type": "file"
},
{
"path": "../proto/music_agregator/indexer/v1/indexer.proto",
"type": "file"
}
]
},
+6 -1
View File
@@ -54,9 +54,14 @@ func serveGrpc(config config.Config) {
var opts []grpc.ServerOption
server := grpc.NewServer(opts...)
indexerServer, err := indexer.NewIndexerServer(config)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create IndexerServer")
}
services := []internal.Registrable{
hello.NewHelloServer(),
indexer.NewIndexerServer(),
indexerServer,
}
for _, service := range services {
+5 -5
View File
@@ -7,11 +7,11 @@ database:
metadata:
endpoint: "http://localhost:50051"
indexers:
- name: "Jackett"
indexer_type: jackett # jackett, prowlarr, or torznab
url: "http://localhost:9117"
api_key: "your-jackett-api-key"
indexer:
name: "Jackett"
indexer_type: jackett # jackett, prowlarr, or torznab
url: "http://localhost:9117"
api_key: "your-jackett-api-key"
# Torrent client - choose one of: qbittorrent, stub, none
torrent:
+33 -1
View File
@@ -1,10 +1,42 @@
package config
import (
"fmt"
)
const (
IndexerTypeJackett IndexerType = "jackett"
)
type IndexerType string
type Config struct {
App struct {
Port string `yaml:"port"`
Host string `yaml:"host"`
Port string `yaml:"port"`
} `yaml:"app"`
Indexer struct {
Url string `yaml:"url"`
Port string `yaml:"port"`
Type IndexerType `yaml:"type"`
ApiKey string `yaml:"api_key"`
} `yaml:"indexer"`
}
func (t *IndexerType) UnmarshalYAML(unmarshal func(any) error) error {
var value string
if err := unmarshal(&value); err != nil {
return err
}
switch IndexerType(value) {
case IndexerTypeJackett:
*t = IndexerType(value)
return nil
default:
return fmt.Errorf("unknown indexer type: %s", value)
}
}
func NewConfig() *Config {
+103 -1
View File
@@ -1,6 +1,108 @@
package indexer
import ()
import (
"encoding/xml"
"strings"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
)
type Indexer interface {
Search()
Capabilities(indexerName string) (IndexerCapabilities, error)
}
type IndexerCapabilities struct {
XMLName xml.Name `xml:"caps"`
Server Server `xml:"server"`
Limits Limits `xml:"limits"`
Searching Searching `xml:"searching"`
Categories []Category `xml:"categories>category"`
}
type Server struct {
Title string `xml:"title,attr"`
}
type Limits struct {
Default int `xml:"default,attr"`
Max int `xml:"max,attr"`
}
type Searching struct {
Search SearchCapability `xml:"search"`
TvSearch SearchCapability `xml:"tv-search"`
MovieSearch SearchCapability `xml:"movie-search"`
MusicSearch SearchCapability `xml:"music-search"`
AudioSearch SearchCapability `xml:"audio-search"`
BookSearch SearchCapability `xml:"book-search"`
}
type SearchCapability struct {
Available string `xml:"available,attr"`
SupportedParams string `xml:"supportedParams,attr"`
SearchEngine string `xml:"searchEngine,attr"`
}
type Category struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
Subcats []Subcat `xml:"subcat"`
}
type Subcat struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
}
func (c *IndexerCapabilities) ToProto() *pb.CapabilitiesResponse {
return &pb.CapabilitiesResponse{
Server: &pb.Server{
Title: c.Server.Title,
},
Limits: &pb.Limits{
Default: int32(c.Limits.Default),
Max: int32(c.Limits.Max),
},
Searching: &pb.Searching{
Search: c.Searching.Search.toProto(),
TvSearch: c.Searching.TvSearch.toProto(),
MovieSearch: c.Searching.MovieSearch.toProto(),
MusicSearch: c.Searching.MusicSearch.toProto(),
AudioSearch: c.Searching.AudioSearch.toProto(),
BookSearch: c.Searching.BookSearch.toProto(),
},
Categories: c.categoriesToProto(),
}
}
func (s *SearchCapability) toProto() *pb.SearchCapability {
var params []string
if s.SupportedParams != "" {
params = strings.Split(s.SupportedParams, ",")
}
return &pb.SearchCapability{
Available: s.Available == "yes",
SupportedParams: params,
SearchEngine: s.SearchEngine,
}
}
func (c *IndexerCapabilities) categoriesToProto() []*pb.Category {
categories := make([]*pb.Category, len(c.Categories))
for i, cat := range c.Categories {
subcats := make([]*pb.Subcat, len(cat.Subcats))
for j, sub := range cat.Subcats {
subcats[j] = &pb.Subcat{
Id: int32(sub.ID),
Name: sub.Name,
}
}
categories[i] = &pb.Category{
Id: int32(cat.ID),
Name: cat.Name,
Subcats: subcats,
}
}
return categories
}
+64
View File
@@ -0,0 +1,64 @@
package indexer
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
"github.com/rs/zerolog/log"
"homelab.lan/music-agregator/internal/config"
)
type JacketIndexer struct {
cfg config.Config
client *http.Client
}
func NewIndexer(cfg config.Config) Indexer {
return &JacketIndexer{
cfg: cfg,
client: &http.Client{
Timeout: time.Second * 10,
},
}
}
func (indexer *JacketIndexer) Search() {
log.Warn().Msg("Unimplemented method search on the Jacket Indexer")
}
func (indexer *JacketIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) {
url := indexer.cfg.Indexer.Url
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab/api?apikey=%v&t=caps", url, indexerName, indexer.cfg.Indexer.ApiKey)
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
log.Error().Err(err).Msg("Error creating request")
return IndexerCapabilities{}, err
}
resp, err := indexer.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Error making capabilities request")
return IndexerCapabilities{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Error reading response body")
return IndexerCapabilities{}, err
}
var capabilities IndexerCapabilities
if err := xml.Unmarshal(body, &capabilities); err != nil {
log.Error().Err(err).Msg("Error parsing capabilities XML")
return IndexerCapabilities{}, err
}
log.Debug().Str("server", capabilities.Server.Title).Msg("Parsed capabilities")
return capabilities, nil
}
+22 -2
View File
@@ -2,23 +2,43 @@ package indexer
import (
"context"
"fmt"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
"homelab.lan/music-agregator/internal/config"
)
type IndexerServer struct {
indexer Indexer
pb.UnimplementedIndexerServiceServer
}
func NewIndexerServer() *IndexerServer {
return &IndexerServer{}
func NewIndexerServer(cfg config.Config) (*IndexerServer, error) {
switch cfg.Indexer.Type {
case config.IndexerTypeJackett:
indexer := NewIndexer(cfg)
return &IndexerServer{indexer: indexer}, nil
default:
return nil, fmt.Errorf("Unable to create the indexer for type: %v", cfg.Indexer.Type)
}
}
func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) {
return &pb.SearchResponse{}, nil
}
func (server *IndexerServer) Capabilities(ctx context.Context, req *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
capabilities, err := server.indexer.Capabilities(req.GetIndexer())
if err != nil {
log.Error().Err(err).Msg("Failed to get capabilities from indexer")
return nil, err
}
return capabilities.ToProto(), nil
}
func (s *IndexerServer) Register(server *grpc.Server) {
pb.RegisterIndexerServiceServer(server, s)
}
+55 -4
View File
@@ -3,9 +3,60 @@ package music_agregator.indexer.v1;
option go_package = "homelab.lan/music-agregator/gen/music_agregator/v1/indexer";
service IndexerService {
rpc Search(SearchRequest) returns (SearchResponse){
}
rpc Search(SearchRequest) returns (SearchResponse) {}
rpc Capabilities(CapabilitiesRequest) returns (CapabilitiesResponse) {}
}
message SearchRequest {}
message SearchResponse {}
message SearchRequest {
string indexer = 1;
string query = 2;
int32 limit = 3;
}
message SearchResponse {
}
message CapabilitiesRequest {
string indexer = 1;
}
message CapabilitiesResponse {
Server server = 1;
Limits limits = 2;
Searching searching = 3;
repeated Category categories = 4;
}
message Server {
string title = 1;
}
message Limits {
int32 default = 1;
int32 max = 2;
}
message Searching {
SearchCapability search = 1;
SearchCapability tv_search = 2;
SearchCapability movie_search = 3;
SearchCapability music_search = 4;
SearchCapability audio_search = 5;
SearchCapability book_search = 6;
}
message SearchCapability {
bool available = 1;
repeated string supported_params = 2;
string search_engine = 3;
}
message Category {
int32 id = 1;
string name = 2;
repeated Subcat subcats = 3;
}
message Subcat {
int32 id = 1;
string name = 2;
}