Add Jacket indexer with capabilities implemented
This commit is contained in:
@@ -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"
|
||||
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Indexer
|
||||
seq: 7
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Jackett
|
||||
seq: 5
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -13,6 +13,10 @@
|
||||
{
|
||||
"path": "../proto/music_agregator/hello/v1/service.proto",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "../proto/music_agregator/indexer/v1/indexer.proto",
|
||||
"type": "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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user