Compare commits
3 Commits
2041c154cf
...
84a6fe8ec7
| Author | SHA1 | Date | |
|---|---|---|---|
| 84a6fe8ec7 | |||
| 8ad2734964 | |||
| 79f3f145de |
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Get Artist Albums
|
||||
type: grpc
|
||||
seq: 3
|
||||
}
|
||||
|
||||
grpc {
|
||||
url: localhost:3000
|
||||
method: /metadata.v1.MetadataService/GetArtistAlbums
|
||||
body: grpc
|
||||
auth: inherit
|
||||
methodType: unary
|
||||
}
|
||||
|
||||
body:grpc {
|
||||
name: message 1
|
||||
content: '''
|
||||
{
|
||||
"artist_id": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Get Artist
|
||||
type: grpc
|
||||
seq: 2
|
||||
}
|
||||
|
||||
grpc {
|
||||
url: localhost:3000
|
||||
method: /metadata.v1.MetadataService/GetArtist
|
||||
body: grpc
|
||||
auth: inherit
|
||||
methodType: unary
|
||||
}
|
||||
|
||||
body:grpc {
|
||||
name: message 1
|
||||
content: '''
|
||||
{
|
||||
"id": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Search Artists
|
||||
type: grpc
|
||||
seq: 1
|
||||
}
|
||||
|
||||
grpc {
|
||||
url: localhost:3000
|
||||
method: /metadata.v1.MetadataService/SearchArtists
|
||||
body: grpc
|
||||
auth: inherit
|
||||
methodType: unary
|
||||
}
|
||||
|
||||
body:grpc {
|
||||
name: message 1
|
||||
content: '''
|
||||
{
|
||||
"query": "Metallica"
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Metadata Agregator
|
||||
seq: 6
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Monitor Album
|
||||
type: grpc
|
||||
seq: 5
|
||||
}
|
||||
|
||||
grpc {
|
||||
url: localhost:3000
|
||||
method: /music_agregator.v1.MusicAgregatorService/MonitorAlbum
|
||||
body: grpc
|
||||
auth: inherit
|
||||
methodType: unary
|
||||
}
|
||||
|
||||
body:grpc {
|
||||
name: message 1
|
||||
content: '''
|
||||
{
|
||||
"album_id": "a0b7b436-94db-4df6-8c5f-bc0e5932a90e",
|
||||
"indexer_options": {
|
||||
"tracker": ""
|
||||
},
|
||||
"quality_type": 1
|
||||
}
|
||||
'''
|
||||
}
|
||||
+2
-2
@@ -3,9 +3,9 @@ inputs:
|
||||
- directory: proto
|
||||
- directory: ../metadata-agregator/proto
|
||||
plugins:
|
||||
- remote: buf.build/protocolbuffers/go
|
||||
- local: protoc-gen-go
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
- remote: buf.build/grpc/go
|
||||
- local: protoc-gen-go-grpc
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
|
||||
@@ -10,12 +10,17 @@ import (
|
||||
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/riverqueue/river/riverdriver/riverpgxv5"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
@@ -63,6 +68,38 @@ func interceptorLogger(l zerolog.Logger) logging.Logger {
|
||||
})
|
||||
}
|
||||
|
||||
func setupRiver(ctx context.Context, cfg config.Config) (*river.Client[pgx.Tx], *pgxpool.Pool) {
|
||||
if !cfg.Indexer.Cache.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dbPool, err := pgxpool.New(ctx, cfg.Database.URL)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to connect to database for River")
|
||||
}
|
||||
|
||||
workers := river.NewWorkers()
|
||||
river.AddWorker(workers, &indexer.CacheRefreshWorker{})
|
||||
|
||||
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{
|
||||
Queues: map[string]river.QueueConfig{
|
||||
river.QueueDefault: {MaxWorkers: 2},
|
||||
},
|
||||
Workers: workers,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create River client")
|
||||
}
|
||||
|
||||
if err := riverClient.Start(ctx); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to start River client")
|
||||
}
|
||||
|
||||
log.Info().Msg("River queue started")
|
||||
|
||||
return riverClient, dbPool
|
||||
}
|
||||
|
||||
func serveGrpc(config config.Config) {
|
||||
srvMetrics := grpcprom.NewServerMetrics(
|
||||
grpcprom.WithServerHandlingTimeHistogram(
|
||||
@@ -94,7 +131,17 @@ func serveGrpc(config config.Config) {
|
||||
),
|
||||
)
|
||||
|
||||
indexerServer, err := indexer.NewIndexerServer(config)
|
||||
ctx := context.Background()
|
||||
riverClient, dbPool := setupRiver(ctx, config)
|
||||
if dbPool != nil {
|
||||
defer dbPool.Close()
|
||||
}
|
||||
|
||||
musiscAgregatorSeerver, err := internal.NewMusicAgregatorServer(config, riverClient)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create MusicAgregatorServer")
|
||||
}
|
||||
indexerServer, err := indexer.NewIndexerServer(config, riverClient)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create IndexerServer")
|
||||
}
|
||||
@@ -112,6 +159,7 @@ func serveGrpc(config config.Config) {
|
||||
indexerServer,
|
||||
torrentServer,
|
||||
metadataServer,
|
||||
musiscAgregatorSeerver,
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
@@ -120,6 +168,7 @@ func serveGrpc(config config.Config) {
|
||||
|
||||
srvMetrics.InitializeMetrics(server)
|
||||
prometheus.MustRegister(srvMetrics)
|
||||
reflection.Register(server)
|
||||
|
||||
go func() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -5,28 +5,125 @@ go 1.26.2
|
||||
require github.com/rs/zerolog v1.35.1
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8 // indirect
|
||||
github.com/anacrolix/chansync v0.7.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.23.0 // indirect
|
||||
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b // indirect
|
||||
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
|
||||
github.com/anacrolix/mmsg v1.0.1 // indirect
|
||||
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1 // indirect
|
||||
github.com/anacrolix/torrent v1.61.0 // indirect
|
||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||
github.com/anacrolix/utp v0.1.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.0.6 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.18 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pion/webrtc/v4 v4.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/riverqueue/river v0.35.1 // indirect
|
||||
github.com/riverqueue/river/riverdriver v0.35.1 // indirect
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1 // indirect
|
||||
github.com/riverqueue/river/rivershared v0.35.1 // indirect
|
||||
github.com/riverqueue/river/rivertype v0.35.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.1.6 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.21.1 // indirect
|
||||
zombiezen.com/go/sqlite v0.13.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/grpc v1.81.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
||||
@@ -1,23 +1,212 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
|
||||
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8 h1:c02PsmoaChabVqAFm7pqPI1UIkDdDAjUaWa6ZmfxybQ=
|
||||
github.com/anacrolix/btree v0.0.0-20251201064447-d86c3fa41bd8/go.mod h1:7stWJ39LeusmMI8mjJuhFNRqep//vx0AsaySRoK9or0=
|
||||
github.com/anacrolix/chansync v0.7.0 h1:wgwxbsJRmOqNjil4INpxHrDp4rlqQhECxR8/WBP4Et0=
|
||||
github.com/anacrolix/chansync v0.7.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||
github.com/anacrolix/dht/v2 v2.23.0 h1:EuD17ykTTEkAMPLjBsS5QjGOwuBgLTdQhds6zPAjeVY=
|
||||
github.com/anacrolix/dht/v2 v2.23.0/go.mod h1:seXRz6HLw8zEnxlysf9ye2eQbrKUmch6PyOHpe/Nb/U=
|
||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||
github.com/anacrolix/envpprof v1.4.0 h1:QHeIcrgHcRChhnxR8l6rlaLlRQx9zd7Q2NII6Zbt83w=
|
||||
github.com/anacrolix/envpprof v1.4.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
|
||||
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b h1:Kuvx/A/TTJuT9x8mn7DeGx2KW9tWn1LI8bira67xdT0=
|
||||
github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b/go.mod h1:NGehhfeXJPBujPx0s6cstSj8B+TERsTY32Xckfx5ftc=
|
||||
github.com/anacrolix/go-libutp v1.3.2 h1:WswiaxTIogchbkzNgGHuHRfbrYLpv4o290mlvcx+++M=
|
||||
github.com/anacrolix/go-libutp v1.3.2/go.mod h1:fCUiEnXJSe3jsPG554A200Qv+45ZzIIyGEvE56SHmyA=
|
||||
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
|
||||
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb h1:nGNLCQbxFQZz7/9PXLGQ9GmavI/W+eX66pSwVeUwugU=
|
||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb/go.mod h1:YjBZbwe2v3RsU7WdoBlVSPVpfKuOAno9SRQ/8tIl+hk=
|
||||
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
||||
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
||||
github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||
github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||
github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
|
||||
github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw=
|
||||
github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc=
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw=
|
||||
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
||||
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
||||
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
||||
github.com/anacrolix/missinggo/v2 v2.10.0 h1:pg0iO4Z/UhP2MAnmGcaMtp5ZP9kyWsusENWN9aolrkY=
|
||||
github.com/anacrolix/missinggo/v2 v2.10.0/go.mod h1:nCRMW6bRCMOVcw5z9BnSYKF+kDbtenx+hQuphf4bK8Y=
|
||||
github.com/anacrolix/mmsg v1.0.1 h1:TxfpV7kX70m3f/O7ielL/2I3OFkMPjrRCPo7+4X5AWw=
|
||||
github.com/anacrolix/mmsg v1.0.1/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
|
||||
github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
|
||||
github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
|
||||
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
||||
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
|
||||
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
|
||||
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
|
||||
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1 h1:oLCfNgEOR3/Z98mSwmwTM1pcqCDb/1zIjxCNn7dzVaE=
|
||||
github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1/go.mod h1:21cUWerw9eiu/3T3kyoChu37AVO+YFue1/H15qqubS0=
|
||||
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/torrent v1.61.0 h1:vxo+B4SwnoP5AQWbhvnTYIaTgPSX+llYUVuQVsN4Jg8=
|
||||
github.com/anacrolix/torrent v1.61.0/go.mod h1:yKUKuZSSDdyOsCbuH+rDOpswl/g546gICapdrU7aUmQ=
|
||||
github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
||||
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
|
||||
github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bsAMtmAfnxXltm+6eBzrrS7SZ52c3SedsulaMI=
|
||||
github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
|
||||
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
|
||||
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
|
||||
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
|
||||
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421 h1:GClwZI0at7xwV0TpgUMTYr/DoTE7TJZ/tc29LcPcs7o=
|
||||
github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -28,22 +217,175 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
||||
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
||||
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
||||
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
||||
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/riverqueue/river v0.35.1 h1:TK1LLGRdTWL7ARPbIUB+TqMnTYJ0GiCoy5Q/yEf5yBE=
|
||||
github.com/riverqueue/river v0.35.1/go.mod h1:jDt0LimObI+5e6FVy7LyuIWfHftmV0wARmiK7W+9D64=
|
||||
github.com/riverqueue/river/riverdriver v0.35.1 h1:zJx8SaQdMP7zVEfd8SDoe8KjVHCXoXoFfzt6v+SJtQg=
|
||||
github.com/riverqueue/river/riverdriver v0.35.1/go.mod h1:Y+rQzz0uvh+pQI+mzJh3qgAGGNxestOWgjKa7mob87w=
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1 h1:GL+ztwpXgIqBin/3wNzq8h1/H8befxl61/DlLvVCAAY=
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1/go.mod h1:5Llh5ONCFsW67dLm5+OelSWTKhliQ989JLbVMwyuN2U=
|
||||
github.com/riverqueue/river/rivershared v0.35.1 h1:XEHf7yj35p5Os5r6K08q9BVaAKsvWhP9hfxEr+MwXqg=
|
||||
github.com/riverqueue/river/rivershared v0.35.1/go.mod h1:YqVk7bZoojLsx58kyQ6ZU2FHP91HP4whVj6MTCtih/c=
|
||||
github.com/riverqueue/river/rivertype v0.35.1 h1:7SfjZ3Hkr7gRjItMHAUzJBAHIqx41yS/4yjVPQVtNfM=
|
||||
github.com/riverqueue/river/rivertype v0.35.1/go.mod h1:D1Ad+EaZiaXbQbJcJcfeicXJMBKno0n6UcfKI5Q7DIQ=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
|
||||
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
@@ -56,25 +398,171 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
|
||||
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
|
||||
zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,11 +22,16 @@ type Config struct {
|
||||
Port string `yaml:"port"`
|
||||
} `yaml:"app"`
|
||||
|
||||
Database struct {
|
||||
URL string `yaml:"url"`
|
||||
} `yaml:"database"`
|
||||
|
||||
Indexer struct {
|
||||
Url string `yaml:"url"`
|
||||
Port string `yaml:"port"`
|
||||
Type IndexerType `yaml:"type"`
|
||||
ApiKey string `yaml:"api_key"`
|
||||
Cache CacheConfig `yaml:"cache"`
|
||||
} `yaml:"indexer"`
|
||||
|
||||
Torrent struct {
|
||||
@@ -40,6 +46,12 @@ type Config struct {
|
||||
} `yaml:"metadata"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
RefreshInterval time.Duration `yaml:"refresh_interval"`
|
||||
TTL time.Duration `yaml:"ttl"`
|
||||
}
|
||||
|
||||
func (t *IndexerType) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var value string
|
||||
if err := unmarshal(&value); err != nil {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type CacheEntry struct {
|
||||
Key string
|
||||
URL string
|
||||
Result SearchResult
|
||||
CreatedAt time.Time
|
||||
TTL time.Duration
|
||||
RefreshInterval time.Duration
|
||||
}
|
||||
|
||||
func (e *CacheEntry) IsExpired() bool {
|
||||
return time.Now().After(e.CreatedAt.Add(e.TTL))
|
||||
}
|
||||
|
||||
type IndexerCache struct {
|
||||
entries map[string]*CacheEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewIndexerCache() *IndexerCache {
|
||||
return &IndexerCache{
|
||||
entries: make(map[string]*CacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IndexerCache) Get(key string) (*CacheEntry, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entry, ok := c.entries[key]
|
||||
if !ok {
|
||||
log.Trace().Str("key", key).Msg("cache miss")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if entry.IsExpired() {
|
||||
log.Trace().Str("key", key).Msg("cache expired")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
log.Trace().Str("key", key).Int("items", len(entry.Result.Items)).Msg("cache hit")
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (c *IndexerCache) Add(entry CacheEntry) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.entries[entry.Key] = &entry
|
||||
log.Debug().Str("key", entry.Key).Int("items", len(entry.Result.Items)).Dur("ttl", entry.TTL).Dur("refresh", entry.RefreshInterval).Msg("cache entry added")
|
||||
}
|
||||
|
||||
func (c *IndexerCache) Update(key string, result SearchResult) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if entry, ok := c.entries[key]; ok {
|
||||
entry.Result = result
|
||||
log.Debug().Str("key", key).Int("items", len(result.Items)).Msg("cache entry updated")
|
||||
} else {
|
||||
log.Warn().Str("key", key).Msg("cache update for missing entry")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IndexerCache) Remove(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
delete(c.entries, key)
|
||||
log.Debug().Str("key", key).Msg("cache entry removed")
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type CacheRefreshArgs struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
TTLExpires time.Time `json:"ttl_expires"`
|
||||
RefreshInterval time.Duration `json:"refresh_interval"`
|
||||
}
|
||||
|
||||
func (CacheRefreshArgs) Kind() string { return "indexer_cache_refresh" }
|
||||
|
||||
type CacheRefreshWorker struct {
|
||||
river.WorkerDefaults[CacheRefreshArgs]
|
||||
Cache *IndexerCache
|
||||
Indexer Indexer
|
||||
RiverClient *river.Client[pgx.Tx]
|
||||
}
|
||||
|
||||
func (w *CacheRefreshWorker) Work(ctx context.Context, job *river.Job[CacheRefreshArgs]) error {
|
||||
args := job.Args
|
||||
log.Trace().Str("key", args.Key).Int64("job_id", job.ID).Time("ttl_expires", args.TTLExpires).Msg("cache refresh worker started")
|
||||
|
||||
if time.Now().After(args.TTLExpires) {
|
||||
w.Cache.Remove(args.Key)
|
||||
log.Debug().Str("key", args.Key).Msg("cache entry TTL expired, removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace().Str("key", args.Key).Str("url", args.URL).Msg("fetching fresh data from indexer")
|
||||
start := time.Now()
|
||||
result, err := w.Indexer.FetchURL(args.URL)
|
||||
if err != nil {
|
||||
retryAt := time.Now().Add(5 * time.Minute)
|
||||
log.Error().Err(err).Str("key", args.Key).Time("retry_at", retryAt).Msg("cache refresh failed, scheduling retry")
|
||||
w.RiverClient.Insert(ctx, args, &river.InsertOpts{
|
||||
ScheduledAt: retryAt,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
log.Trace().Str("key", args.Key).Int("items", len(result.Items)).Dur("duration", time.Since(start)).Msg("fresh data fetched")
|
||||
|
||||
w.Cache.Update(args.Key, result)
|
||||
|
||||
nextRefresh := time.Now().Add(args.RefreshInterval)
|
||||
_, err = w.RiverClient.Insert(ctx, args, &river.InsertOpts{
|
||||
ScheduledAt: nextRefresh,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", args.Key).Msg("failed to schedule next cache refresh")
|
||||
} else {
|
||||
log.Trace().Str("key", args.Key).Time("next_refresh", nextRefresh).Msg("next refresh scheduled")
|
||||
}
|
||||
|
||||
log.Debug().Str("key", args.Key).Int("items", len(result.Items)).Msg("cache refreshed")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"homelab.lan/music-agregator/internal/config"
|
||||
)
|
||||
|
||||
type CachedIndexer struct {
|
||||
inner Indexer
|
||||
cache *IndexerCache
|
||||
riverClient *river.Client[pgx.Tx]
|
||||
cfg config.CacheConfig
|
||||
}
|
||||
|
||||
func NewCachedIndexer(inner Indexer, cache *IndexerCache, riverClient *river.Client[pgx.Tx], cfg config.CacheConfig) *CachedIndexer {
|
||||
return &CachedIndexer{
|
||||
inner: inner,
|
||||
cache: cache,
|
||||
riverClient: riverClient,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
|
||||
key := query + "|" + tracker
|
||||
log.Trace().Str("key", key).Str("query", query).Str("tracker", tracker).Msg("cached indexer search")
|
||||
|
||||
if entry, ok := c.cache.Get(key); ok {
|
||||
log.Debug().Str("key", key).Int("items", len(entry.Result.Items)).Msg("returning cached result")
|
||||
return entry.Result, nil
|
||||
}
|
||||
|
||||
log.Trace().Str("key", key).Msg("cache miss, fetching from indexer")
|
||||
result, err := c.inner.Search(query, limit, tracker)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", key).Msg("cached indexer fetch failed")
|
||||
return SearchResult{}, err
|
||||
}
|
||||
|
||||
url := c.inner.BuildSearchURL(query, limit, tracker)
|
||||
log.Trace().Str("key", key).Str("url", url).Int("items", len(result.Items)).Msg("caching result")
|
||||
|
||||
c.cache.Add(CacheEntry{
|
||||
Key: key,
|
||||
URL: url,
|
||||
Result: result,
|
||||
CreatedAt: time.Now(),
|
||||
TTL: c.cfg.TTL,
|
||||
RefreshInterval: c.cfg.RefreshInterval,
|
||||
})
|
||||
|
||||
scheduleAt := time.Now().Add(c.cfg.RefreshInterval)
|
||||
_, err = c.riverClient.Insert(context.Background(), CacheRefreshArgs{
|
||||
Key: key,
|
||||
URL: url,
|
||||
TTLExpires: time.Now().Add(c.cfg.TTL),
|
||||
RefreshInterval: c.cfg.RefreshInterval,
|
||||
}, &river.InsertOpts{
|
||||
ScheduledAt: scheduleAt,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", key).Msg("failed to schedule cache refresh job")
|
||||
} else {
|
||||
log.Debug().Str("key", key).Time("scheduled_at", scheduleAt).Msg("cache refresh job scheduled")
|
||||
}
|
||||
|
||||
log.Debug().Str("key", key).Dur("ttl", c.cfg.TTL).Dur("refresh", c.cfg.RefreshInterval).Int("items", len(result.Items)).Msg("cached indexer search complete")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *CachedIndexer) FetchURL(url string) (SearchResult, error) {
|
||||
log.Trace().Str("url", url).Msg("cached indexer fetch URL passthrough")
|
||||
return c.inner.FetchURL(url)
|
||||
}
|
||||
|
||||
func (c *CachedIndexer) BuildSearchURL(query string, limit int32, tracker string) string {
|
||||
return c.inner.BuildSearchURL(query, limit, tracker)
|
||||
}
|
||||
|
||||
func (c *CachedIndexer) Capabilities(indexerName string) (IndexerCapabilities, error) {
|
||||
log.Trace().Str("indexer", indexerName).Msg("cached indexer capabilities passthrough")
|
||||
return c.inner.Capabilities(indexerName)
|
||||
}
|
||||
@@ -2,5 +2,7 @@ package indexer
|
||||
|
||||
type Indexer interface {
|
||||
Search(query string, limit int32, indexer string) (SearchResult, error)
|
||||
FetchURL(url string) (SearchResult, error)
|
||||
BuildSearchURL(query string, limit int32, tracker string) string
|
||||
Capabilities(indexerName string) (IndexerCapabilities, error)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -20,21 +21,42 @@ func NewIndexer(cfg config.Config) Indexer {
|
||||
return &JacketIndexer{
|
||||
cfg: cfg,
|
||||
client: &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Timeout: 60 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
|
||||
func (indexer *JacketIndexer) BuildSearchURL(query string, limit int32, tracker string) string {
|
||||
searchTracker := "all"
|
||||
if len(tracker) != 0 {
|
||||
searchTracker = tracker
|
||||
}
|
||||
|
||||
url := indexer.cfg.Indexer.Url
|
||||
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&limit=%d&cat=3010,3040&q=%v&t=search", url, searchTracker, indexer.cfg.Indexer.ApiKey, limit, query)
|
||||
uri := fmt.Sprintf("%v/api/v2.0/indexers/%v/results/torznab?apikey=%v&cat=3010,3040&q=%v&t=search",
|
||||
indexer.cfg.Indexer.Url, searchTracker, indexer.cfg.Indexer.ApiKey, url.QueryEscape(query))
|
||||
if limit > 0 {
|
||||
uri += fmt.Sprintf("&limit=%d", limit)
|
||||
}
|
||||
|
||||
log.Trace().Str("tracker", searchTracker).Str("query", query).Int32("limit", limit).Msg("jackett request")
|
||||
return uri
|
||||
}
|
||||
|
||||
func (indexer *JacketIndexer) Search(query string, limit int32, tracker string) (SearchResult, error) {
|
||||
uri := indexer.BuildSearchURL(query, limit, tracker)
|
||||
return indexer.FetchURL(uri)
|
||||
}
|
||||
|
||||
type JackettError struct {
|
||||
Code string `xml:"code,attr"`
|
||||
Description string `xml:"description,attr"`
|
||||
}
|
||||
|
||||
func (e *JackettError) Error() string {
|
||||
return fmt.Sprintf("jackett error %s: %s", e.Code, e.Description)
|
||||
}
|
||||
|
||||
func (indexer *JacketIndexer) FetchURL(uri string) (SearchResult, error) {
|
||||
log.Trace().Str("uri", uri).Msg("jackett request")
|
||||
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
@@ -62,6 +84,15 @@ func (indexer *JacketIndexer) Search(query string, limit int32, tracker string)
|
||||
Dur("duration", time.Since(start)).
|
||||
Msg("jackett response")
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var jackettErr JackettError
|
||||
if xmlErr := xml.Unmarshal(body, &jackettErr); xmlErr == nil && jackettErr.Code != "" {
|
||||
log.Error().Str("code", jackettErr.Code).Str("description", jackettErr.Description).Msg("jackett returned error")
|
||||
return SearchResult{}, &jackettErr
|
||||
}
|
||||
return SearchResult{}, fmt.Errorf("jackett returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var searchResult SearchResult
|
||||
if err := xml.Unmarshal(body, &searchResult); err != nil {
|
||||
log.Error().Err(err).Msg("error parsing search XML")
|
||||
|
||||
+68
-31
@@ -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}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package indexer
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
@@ -15,10 +17,10 @@ type IndexerServer struct {
|
||||
pb.UnimplementedIndexerServiceServer
|
||||
}
|
||||
|
||||
func NewIndexerServer(cfg config.Config) (*IndexerServer, error) {
|
||||
service, err := NewIndexerService(cfg)
|
||||
func NewIndexerServer(cfg config.Config, riverClient *river.Client[pgx.Tx]) (*IndexerServer, error) {
|
||||
service, err := NewIndexerService(cfg, riverClient)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to initialize IndexerService")
|
||||
log.Err(err).Msg("failed to initialize IndexerService")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -32,7 +34,9 @@ func (server *IndexerServer) Search(ctx context.Context, req *pb.SearchRequest)
|
||||
Str("tracker", req.GetTracker()).
|
||||
Msg("search started")
|
||||
|
||||
resp, err := server.service.Search(req)
|
||||
log.Trace().Str("query", req.GetQuery()).Msg("fetching results from indexer")
|
||||
|
||||
resp, err := server.service.Search(req.GetQuery(), req.GetLimit(), req.GetTracker())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("query", req.GetQuery()).Msg("search failed")
|
||||
return nil, err
|
||||
@@ -40,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) {
|
||||
|
||||
+19
-10
@@ -3,6 +3,8 @@ package indexer
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
pb "homelab.lan/music-agregator/gen/music_agregator/indexer/v1"
|
||||
@@ -13,28 +15,35 @@ type IndexerService struct {
|
||||
indexer Indexer
|
||||
}
|
||||
|
||||
func NewIndexerService(cfg config.Config) (*IndexerService, error) {
|
||||
func NewIndexerService(cfg config.Config, riverClient *river.Client[pgx.Tx]) (*IndexerService, error) {
|
||||
var idx Indexer
|
||||
|
||||
switch cfg.Indexer.Type {
|
||||
case config.IndexerTypeJackett:
|
||||
indexer := NewIndexer(cfg)
|
||||
return &IndexerService{indexer: indexer}, nil
|
||||
idx = NewIndexer(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unable to create the indexer for type: %v", cfg.Indexer.Type)
|
||||
return nil, fmt.Errorf("unable to create the indexer for type: %v", cfg.Indexer.Type)
|
||||
}
|
||||
|
||||
if cfg.Indexer.Cache.Enabled && riverClient != nil {
|
||||
cache := NewIndexerCache()
|
||||
idx = NewCachedIndexer(idx, cache, riverClient, cfg.Indexer.Cache)
|
||||
log.Info().Dur("ttl", cfg.Indexer.Cache.TTL).Dur("refresh", cfg.Indexer.Cache.RefreshInterval).Msg("indexer cache enabled")
|
||||
}
|
||||
|
||||
return &IndexerService{indexer: idx}, nil
|
||||
}
|
||||
|
||||
func (service *IndexerService) Search(req *pb.SearchRequest) (*pb.SearchResponse, error) {
|
||||
log.Trace().Str("query", req.GetQuery()).Msg("fetching results from indexer")
|
||||
|
||||
searchResult, err := service.indexer.Search(req.GetQuery(), req.GetLimit(), req.GetTracker())
|
||||
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) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
pb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
)
|
||||
|
||||
func newMetadataClient(endpoint string) (pb.MetadataServiceClient, *grpc.ClientConn, error) {
|
||||
func NewMetadataClient(endpoint string) (pb.MetadataServiceClient, *grpc.ClientConn, error) {
|
||||
log.Trace().Str("endpoint", endpoint).Msg("connecting to metadata service")
|
||||
|
||||
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
@@ -17,7 +17,7 @@ type MetadataServer struct {
|
||||
}
|
||||
|
||||
func NewMetadataServer(cfg config.Config) (*MetadataServer, error) {
|
||||
client, conn, err := newMetadataClient(cfg.Metadata.Endpoint)
|
||||
client, conn, err := NewMetadataClient(cfg.Metadata.Endpoint)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to initialize MetadataServer")
|
||||
return nil, err
|
||||
@@ -36,7 +36,7 @@ func (s *MetadataServer) Register(server *grpc.Server) {
|
||||
pb.RegisterMetadataServiceServer(server, s)
|
||||
}
|
||||
|
||||
func (s *MetadataServer) GetArtist(ctx context.Context, req *pb.GetArtistRequest) (*pb.Artist, error) {
|
||||
func (s *MetadataServer) GetArtist(ctx context.Context, req *pb.GetArtistRequest) (*pb.GetArtistResponse, error) {
|
||||
log.Debug().Msg("metadata GetArtist")
|
||||
return s.client.GetArtist(ctx, req)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (s *MetadataServer) SearchArtists(ctx context.Context, req *pb.SearchArtist
|
||||
return s.client.SearchArtists(ctx, req)
|
||||
}
|
||||
|
||||
func (s *MetadataServer) GetAlbum(ctx context.Context, req *pb.GetAlbumRequest) (*pb.Album, error) {
|
||||
func (s *MetadataServer) GetAlbum(ctx context.Context, req *pb.GetAlbumRequest) (*pb.GetAlbumResponse, error) {
|
||||
log.Debug().Msg("metadata GetAlbum")
|
||||
return s.client.GetAlbum(ctx, req)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (s *MetadataServer) GetArtistAlbums(ctx context.Context, req *pb.GetArtistA
|
||||
return s.client.GetArtistAlbums(ctx, req)
|
||||
}
|
||||
|
||||
func (s *MetadataServer) GetTrack(ctx context.Context, req *pb.GetTrackRequest) (*pb.Track, error) {
|
||||
func (s *MetadataServer) GetTrack(ctx context.Context, req *pb.GetTrackRequest) (*pb.GetTrackResponse, error) {
|
||||
log.Debug().Msg("metadata GetTrack")
|
||||
return s.client.GetTrack(ctx, req)
|
||||
}
|
||||
@@ -66,6 +66,11 @@ func (s *MetadataServer) GetAlbumTracks(ctx context.Context, req *pb.GetAlbumTra
|
||||
return s.client.GetAlbumTracks(ctx, req)
|
||||
}
|
||||
|
||||
func (s *MetadataServer) SearchAlbums(ctx context.Context, req *pb.SearchAlbumsRequest) (*pb.SearchAlbumsResponse, error) {
|
||||
log.Debug().Str("query", req.GetQuery()).Str("artist", req.GetArtist()).Msg("metadata SearchAlbums")
|
||||
return s.client.SearchAlbums(ctx, req)
|
||||
}
|
||||
|
||||
func (s *MetadataServer) SyncArtist(ctx context.Context, req *pb.SyncArtistRequest) (*pb.SyncArtistResponse, error) {
|
||||
log.Debug().Msg("metadata SyncArtist")
|
||||
return s.client.SyncArtist(ctx, req)
|
||||
|
||||
@@ -165,6 +165,15 @@ type Release struct {
|
||||
Label string
|
||||
CatalogNum string
|
||||
|
||||
InfoHash string
|
||||
TrackCount int
|
||||
TrackNames []string
|
||||
AudioFileCount int
|
||||
TotalAudioSize int64
|
||||
HasCoverArt bool
|
||||
HasCueSheet bool
|
||||
HasRipLog bool
|
||||
|
||||
ParsedSuccessfully bool
|
||||
ParseErrors []string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
|
||||
"homelab.lan/music-agregator/internal/config"
|
||||
)
|
||||
|
||||
type MusicAgregatorServer struct {
|
||||
service *MusicAgregatorService
|
||||
pb.UnimplementedMusicAgregatorServiceServer
|
||||
}
|
||||
|
||||
func NewMusicAgregatorServer(cfg config.Config, riverClient *river.Client[pgx.Tx]) (*MusicAgregatorServer, error) {
|
||||
service, err := NewMusicAgregatorService(cfg, riverClient)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create MusicAgregatorService")
|
||||
return nil, err
|
||||
}
|
||||
return &MusicAgregatorServer{
|
||||
service: service,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MusicAgregatorServer) MonitorAlbum(ctx context.Context, req *pb.MonitorAlbumRequest) (*pb.MonitorAlbumResponse, error) {
|
||||
return s.service.MonitorAlbum(ctx, req)
|
||||
}
|
||||
|
||||
func (s *MusicAgregatorServer) Register(server *grpc.Server) {
|
||||
pb.RegisterMusicAgregatorServiceServer(server, s)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
|
||||
|
||||
"homelab.lan/music-agregator/internal/config"
|
||||
"homelab.lan/music-agregator/internal/indexer"
|
||||
"homelab.lan/music-agregator/internal/metadata"
|
||||
"homelab.lan/music-agregator/internal/release"
|
||||
"homelab.lan/music-agregator/internal/torrent"
|
||||
torrentParser "homelab.lan/music-agregator/internal/tracker"
|
||||
)
|
||||
|
||||
type parsedItem struct {
|
||||
item *indexer.SearchItemResult
|
||||
rel *release.Release
|
||||
torrentData []byte
|
||||
}
|
||||
|
||||
type MusicAgregatorService struct {
|
||||
config config.Config
|
||||
metadataClient metadataPb.MetadataServiceClient
|
||||
metadataConn *grpc.ClientConn
|
||||
indexer *indexer.IndexerService
|
||||
torrentClient torrent.TorrentClient
|
||||
magnetResolver *torrentParser.MagnetResolver
|
||||
}
|
||||
|
||||
func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.Tx]) (*MusicAgregatorService, error) {
|
||||
indexer, err := indexer.NewIndexerService(cfg, riverClient)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create IndexerService")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadataClient, conn, err := metadata.NewMetadataClient(cfg.Metadata.Endpoint)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create metadata client")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
magnetResolver, err := torrentParser.NewMagnetResolver(30 * time.Second)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create magnet resolver")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
torrentClient, err := torrent.NewTorrentClient(cfg)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create torrent client")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MusicAgregatorService{
|
||||
config: cfg,
|
||||
metadataClient: metadataClient,
|
||||
metadataConn: conn,
|
||||
indexer: indexer,
|
||||
torrentClient: torrentClient,
|
||||
magnetResolver: magnetResolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MusicAgregatorService) Close() {
|
||||
if s.metadataConn != nil {
|
||||
s.metadataConn.Close()
|
||||
}
|
||||
if s.magnetResolver != nil {
|
||||
s.magnetResolver.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) MonitorAlbum(ctx context.Context, req *pb.MonitorAlbumRequest) (*pb.MonitorAlbumResponse, error) {
|
||||
album, err := service.fetchAlbumMetadata(ctx, req.GetAlbumId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchResult, err := service.searchIndexer(album, req.GetIndexerOptions().GetTracker())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed := service.parseSearchResults(searchResult, album)
|
||||
|
||||
filtered := filterByQuality(parsed, req.GetQuality())
|
||||
if len(filtered) == 0 {
|
||||
log.Warn().Str("album", album.GetTitle()).Str("quality", req.GetQuality().String()).Msg("no releases match quality filter")
|
||||
return &pb.MonitorAlbumResponse{}, nil
|
||||
}
|
||||
|
||||
best := selectBestRelease(filtered)
|
||||
|
||||
if err := service.addToTorrentClient(best); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.MonitorAlbumResponse{
|
||||
Release: buildMonitoredRelease(best),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) fetchAlbumMetadata(ctx context.Context, albumID string) (*metadataPb.Album, error) {
|
||||
resp, err := service.metadataClient.GetAlbum(ctx, &metadataPb.GetAlbumRequest{
|
||||
Identifier: &metadataPb.GetAlbumRequest_Id{Id: albumID},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("album_id", albumID).Msg("metadata GetAlbum failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
album := resp.GetAlbum()
|
||||
artistName := ""
|
||||
if len(album.GetArtists()) > 0 {
|
||||
artistName = album.GetArtists()[0].GetArtist().GetName()
|
||||
}
|
||||
|
||||
log.Debug().Str("album_id", albumID).Str("title", album.GetTitle()).Str("artist", artistName).Msg("album metadata fetched")
|
||||
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) searchIndexer(album *metadataPb.Album, tracker string) (*indexer.SearchResponse, error) {
|
||||
artistName := ""
|
||||
if len(album.GetArtists()) > 0 {
|
||||
artistName = album.GetArtists()[0].GetArtist().GetName()
|
||||
}
|
||||
|
||||
query := album.GetTitle()
|
||||
if artistName != "" {
|
||||
query = artistName + " " + query
|
||||
}
|
||||
|
||||
if tracker == "" {
|
||||
tracker = "all"
|
||||
}
|
||||
|
||||
result, err := service.indexer.Search(query, -1, tracker)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("query", query).Msg("indexer search failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug().Int("results", len(result.Items)).Str("query", query).Msg("indexer search completed")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) parseSearchResults(searchResult *indexer.SearchResponse, album *metadataPb.Album) []parsedItem {
|
||||
parser := torrentParser.NewGenericParser()
|
||||
var parsed []parsedItem
|
||||
|
||||
for _, item := range searchResult.Items {
|
||||
if item.DownloadLink == "" {
|
||||
log.Trace().Str("title", item.Title).Msg("skipping item without download link")
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Seeders == 0 {
|
||||
log.Warn().Str("title", item.Title).Str("tracker", item.Tracker).Msg("skipping torrent with no seeders")
|
||||
continue
|
||||
}
|
||||
|
||||
r, torrentData := service.resolveRelease(parser, item, album)
|
||||
|
||||
log.Debug().
|
||||
Str("title", item.Title).
|
||||
Str("format", r.Format.String()).
|
||||
Int("tracks", r.TrackCount).
|
||||
Bool("lossless", r.Format.IsLossless()).
|
||||
Int("seeders", item.Seeders).
|
||||
Str("tracker", item.Tracker).
|
||||
Msg("release parsed")
|
||||
|
||||
parsed = append(parsed, parsedItem{item: item, rel: r, torrentData: torrentData})
|
||||
}
|
||||
|
||||
log.Debug().Int("total", len(searchResult.Items)).Int("parsed", len(parsed)).Msg("parsing complete")
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) resolveRelease(parser *torrentParser.GenericParser, item *indexer.SearchItemResult, album *metadataPb.Album) (*release.Release, []byte) {
|
||||
if strings.HasPrefix(item.DownloadLink, "magnet:") {
|
||||
log.Trace().Str("title", item.Title).Int("reported_seeders", item.Seeders).Msg("resolving magnet")
|
||||
torrentData, err := service.magnetResolver.Resolve(item.DownloadLink)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("title", item.Title).Int("reported_seeders", item.Seeders).Msg("magnet resolve failed, falling back to title parse")
|
||||
return parser.Parse(item.Title), nil
|
||||
}
|
||||
return parser.ParseTorrent(torrentData, album), torrentData
|
||||
}
|
||||
|
||||
torrentData, err := downloadTorrentData(item.DownloadLink)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("title", item.Title).Msg("failed to download torrent, falling back to title parse")
|
||||
return parser.Parse(item.Title), nil
|
||||
}
|
||||
return parser.ParseTorrent(torrentData, album), torrentData
|
||||
}
|
||||
|
||||
func filterByQuality(items []parsedItem, quality pb.QualityType) []parsedItem {
|
||||
var filtered []parsedItem
|
||||
for _, p := range items {
|
||||
match := quality == pb.QualityType_QUALITY_UNSPECIFIED ||
|
||||
(quality == pb.QualityType_QUALITY_LOSSLESS && p.rel.Format.IsLossless()) ||
|
||||
(quality == pb.QualityType_QUALITY_LOSSY && !p.rel.Format.IsLossless())
|
||||
|
||||
if !match {
|
||||
log.Debug().Str("title", p.item.Title).Str("format", p.rel.Format.String()).Str("wanted", quality.String()).Msg("filtered out by quality")
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func selectBestRelease(items []parsedItem) parsedItem {
|
||||
best := items[0]
|
||||
for _, p := range items[1:] {
|
||||
if p.item.Seeders > best.item.Seeders {
|
||||
best = p
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("title", best.item.Title).
|
||||
Str("format", best.rel.Format.String()).
|
||||
Int("seeders", best.item.Seeders).
|
||||
Str("tracker", best.item.Tracker).
|
||||
Str("hash", best.rel.InfoHash).
|
||||
Msg("best release selected")
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) addToTorrentClient(best parsedItem) error {
|
||||
if best.rel.InfoHash != "" {
|
||||
existing, err := service.torrentClient.Find(torrent.FindOptions{Hash: best.rel.InfoHash})
|
||||
if err == nil && len(existing) > 0 {
|
||||
log.Info().Str("hash", best.rel.InfoHash).Str("state", existing[0].State).Msg("torrent already exists in client")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(best.item.DownloadLink, "magnet:") {
|
||||
if err := service.torrentClient.AddMagnet(best.item.DownloadLink); err != nil {
|
||||
log.Error().Err(err).Str("title", best.item.Title).Msg("failed to add magnet to client")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if len(best.torrentData) == 0 {
|
||||
log.Error().Str("title", best.item.Title).Msg("no torrent data available")
|
||||
return fmt.Errorf("no torrent data available for best release")
|
||||
}
|
||||
if err := service.torrentClient.AddTorrent(torrent.TorrentFile{
|
||||
Filename: best.rel.Album + ".torrent",
|
||||
Data: best.torrentData,
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Str("title", best.item.Title).Msg("failed to add torrent to client")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Str("title", best.item.Title).Str("hash", best.rel.InfoHash).Msg("torrent added to client")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMonitoredRelease(p parsedItem) *pb.MonitoredRelease {
|
||||
return &pb.MonitoredRelease{
|
||||
InfoHash: p.rel.InfoHash,
|
||||
Artist: p.rel.Artist,
|
||||
Album: p.rel.Album,
|
||||
Year: int32(p.rel.Year),
|
||||
Format: p.rel.Format.String(),
|
||||
Lossless: p.rel.Format.IsLossless(),
|
||||
BitDepth: int32(p.rel.BitDepth),
|
||||
SampleRate: int32(p.rel.SampleRate),
|
||||
Source: p.rel.Source.String(),
|
||||
TrackCount: int32(p.rel.TrackCount),
|
||||
TrackNames: p.rel.TrackNames,
|
||||
HasCoverArt: p.rel.HasCoverArt,
|
||||
HasCueSheet: p.rel.HasCueSheet,
|
||||
HasRipLog: p.rel.HasRipLog,
|
||||
TotalAudioSize: p.rel.TotalAudioSize,
|
||||
DownloadLink: p.item.DownloadLink,
|
||||
Seeders: int32(p.item.Seeders),
|
||||
Tracker: p.item.Tracker,
|
||||
}
|
||||
}
|
||||
|
||||
func downloadTorrentData(url string) ([]byte, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("downloading torrent: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("torrent download returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading torrent data: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -43,5 +43,6 @@ type TorrentClient interface {
|
||||
Login(username string, password string) (string, error)
|
||||
List() ([]TorrentInfo, error)
|
||||
Find(opts FindOptions) ([]TorrentInfo, error)
|
||||
Add(file TorrentFile) error
|
||||
AddTorrent(file TorrentFile) error
|
||||
AddMagnet(magnetURI string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"homelab.lan/music-agregator/internal/config"
|
||||
)
|
||||
|
||||
func NewTorrentClient(cfg config.Config) (TorrentClient, error) {
|
||||
var client TorrentClient
|
||||
|
||||
switch cfg.Torrent.ClientType {
|
||||
case config.TorrentClientQbittorrent:
|
||||
client = NewQbittorrentClient(cfg.Torrent.Url)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown torrent client type: %s", cfg.Torrent.ClientType)
|
||||
}
|
||||
|
||||
if _, err := client.Login(cfg.Torrent.Username, cfg.Torrent.Password); err != nil {
|
||||
return nil, fmt.Errorf("torrent client login failed: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("client", string(cfg.Torrent.ClientType)).Str("url", cfg.Torrent.Url).Msg("torrent client connected")
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -173,8 +173,8 @@ func filterLocally(torrents []TorrentInfo, opts FindOptions) []TorrentInfo {
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *QbittorrentClient) Add(file TorrentFile) error {
|
||||
log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Msg("qbittorrent adding torrent")
|
||||
func (c *QbittorrentClient) AddTorrent(file TorrentFile) error {
|
||||
log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Msg("qbittorrent adding torrent file")
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
@@ -202,6 +202,29 @@ func (c *QbittorrentClient) Add(file TorrentFile) error {
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.AddCookie(&http.Cookie{Name: "SID", Value: c.sid})
|
||||
|
||||
return c.doAdd(req, file.Filename)
|
||||
}
|
||||
|
||||
func (c *QbittorrentClient) AddMagnet(magnetURI string) error {
|
||||
truncated := magnetURI
|
||||
if len(truncated) > 80 {
|
||||
truncated = truncated[:80] + "..."
|
||||
}
|
||||
log.Trace().Str("magnet", truncated).Msg("qbittorrent adding magnet")
|
||||
|
||||
data := url.Values{"urls": {magnetURI}}
|
||||
req, err := http.NewRequest("POST", c.baseURL+"/api/v2/torrents/add", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("qbittorrent creating magnet add request failed")
|
||||
return fmt.Errorf("creating magnet add request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "SID", Value: c.sid})
|
||||
|
||||
return c.doAdd(req, truncated)
|
||||
}
|
||||
|
||||
func (c *QbittorrentClient) doAdd(req *http.Request, label string) error {
|
||||
start := time.Now()
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -219,12 +242,11 @@ func (c *QbittorrentClient) Add(file TorrentFile) error {
|
||||
log.Trace().Int("status", resp.StatusCode).Dur("duration", time.Since(start)).Msg("qbittorrent add response")
|
||||
|
||||
if resp.StatusCode != http.StatusOK || string(body) != "Ok." {
|
||||
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Msg("qbittorrent add torrent failed")
|
||||
return fmt.Errorf("add torrent failed: status %d, body: %s", resp.StatusCode, string(body))
|
||||
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Msg("qbittorrent add failed")
|
||||
return fmt.Errorf("add failed: status %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Info().Str("filename", file.Filename).Msg("qbittorrent torrent added")
|
||||
|
||||
log.Info().Str("label", label).Msg("qbittorrent torrent added")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func (service *TorrentService) Add(req *pb.AddRequest) (*pb.AddResponse, error)
|
||||
return nil, fmt.Errorf("either torrent_data or download_url must be provided")
|
||||
}
|
||||
|
||||
if err := service.client.Add(file); err != nil {
|
||||
if err := service.client.AddTorrent(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
"homelab.lan/music-agregator/internal/release"
|
||||
)
|
||||
|
||||
var (
|
||||
bitratePattern = regexp.MustCompile(`(?i)(\d{2,3})\s*kbps`)
|
||||
hiResPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(\d{1,2})\s*[Bb]it\s*[-/]?\s*(\d{2,3}(?:\.\d)?)\s*[kK][Hh][Zz]`),
|
||||
regexp.MustCompile(`(?i)\[?\s*(?:FLAC|Flac)\s+(\d{1,2})\s*[-/]\s*(\d{2,3}(?:\.\d)?)\s*\]?`),
|
||||
regexp.MustCompile(`(?i)\[?\s*(\d{1,2})\s*[Bb]it\s*\]?`),
|
||||
}
|
||||
sourcePattern = regexp.MustCompile(`(?i)\[(CD|WEB|Vinyl|LP|Cassette|MC|DVD|Blu-?Ray|SACD|DAT)\]`)
|
||||
ripTypePattern = regexp.MustCompile(`(?i)(vinyl\s*rip|SACD[- ]?R|HDCD|DSD\d*|tape\s*rip)`)
|
||||
)
|
||||
|
||||
var audioExtensions = map[string]release.AudioFormat{
|
||||
".flac": release.FormatFLAC,
|
||||
".mp3": release.FormatMP3,
|
||||
".aac": release.FormatAAC,
|
||||
".m4a": release.FormatAAC,
|
||||
".ape": release.FormatAPE,
|
||||
".wv": release.FormatWavPack,
|
||||
".alac": release.FormatALAC,
|
||||
".ogg": release.FormatOGG,
|
||||
".wav": release.FormatWAV,
|
||||
}
|
||||
|
||||
type GenericParser struct{}
|
||||
|
||||
func NewGenericParser() *GenericParser {
|
||||
return &GenericParser{}
|
||||
}
|
||||
|
||||
func (p *GenericParser) ParseTorrent(torrentData []byte, album *metadataPb.Album) *release.Release {
|
||||
r := &release.Release{}
|
||||
|
||||
p.fillFromMetadata(r, album)
|
||||
p.fillFromTorrent(r, torrentData)
|
||||
|
||||
r.ParsedSuccessfully = r.Artist != "" && r.Album != ""
|
||||
if !r.ParsedSuccessfully {
|
||||
r.ParseErrors = append(r.ParseErrors, "missing artist or album")
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *GenericParser) Parse(title string) *release.Release {
|
||||
r := &release.Release{RawTitle: title}
|
||||
|
||||
p.fillFromTitle(r, title)
|
||||
|
||||
r.ParsedSuccessfully = r.Artist != "" && r.Album != ""
|
||||
if !r.ParsedSuccessfully {
|
||||
r.ParseErrors = append(r.ParseErrors, "missing artist or album")
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *GenericParser) fillFromMetadata(r *release.Release, album *metadataPb.Album) {
|
||||
if album == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.Album = album.GetTitle()
|
||||
|
||||
if len(album.GetArtists()) > 0 {
|
||||
r.Artist = album.GetArtists()[0].GetArtist().GetName()
|
||||
}
|
||||
|
||||
if album.GetReleaseDate() != "" {
|
||||
if year, err := strconv.Atoi(album.GetReleaseDate()[:4]); err == nil {
|
||||
r.Year = year
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(album.GetAlbumType()) {
|
||||
case "album":
|
||||
r.Type = release.TypeAlbum
|
||||
case "ep":
|
||||
r.Type = release.TypeEP
|
||||
case "single":
|
||||
r.Type = release.TypeSingle
|
||||
case "compilation":
|
||||
r.Type = release.TypeCompilation
|
||||
case "soundtrack":
|
||||
r.Type = release.TypeSoundtrack
|
||||
case "live":
|
||||
r.Type = release.TypeLive
|
||||
}
|
||||
|
||||
for _, g := range album.GetGenres() {
|
||||
r.Genres = append(r.Genres, g.GetName())
|
||||
}
|
||||
|
||||
if album.GetLabel() != nil {
|
||||
r.Label = album.GetLabel().GetName()
|
||||
}
|
||||
|
||||
r.TrackCount = int(album.GetTotalTracks())
|
||||
r.ReleaseCount = int(album.GetTotalDiscs())
|
||||
|
||||
log.Trace().
|
||||
Str("artist", r.Artist).
|
||||
Str("album", r.Album).
|
||||
Int("year", r.Year).
|
||||
Str("type", r.Type.String()).
|
||||
Msg("filled from metadata")
|
||||
}
|
||||
|
||||
func (p *GenericParser) fillFromTorrent(r *release.Release, torrentData []byte) {
|
||||
if len(torrentData) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mi, err := metainfo.Load(strings.NewReader(string(torrentData)))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to parse torrent data")
|
||||
r.ParseErrors = append(r.ParseErrors, "failed to parse torrent: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
info, err := mi.UnmarshalInfo()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to unmarshal torrent info")
|
||||
r.ParseErrors = append(r.ParseErrors, "failed to unmarshal torrent info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
r.RawTitle = info.Name
|
||||
r.InfoHash = mi.HashInfoBytes().HexString()
|
||||
|
||||
formatCounts := make(map[release.AudioFormat]int)
|
||||
formatSizes := make(map[release.AudioFormat]int64)
|
||||
|
||||
if len(info.Files) == 0 {
|
||||
ext := strings.ToLower(filepath.Ext(info.Name))
|
||||
if fmt, ok := audioExtensions[ext]; ok {
|
||||
r.Format = fmt
|
||||
r.AudioFileCount = 1
|
||||
r.TotalAudioSize = info.Length
|
||||
}
|
||||
} else {
|
||||
for _, f := range info.Files {
|
||||
path := filepath.Join(f.Path...)
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
name := strings.TrimSuffix(filepath.Base(path), ext)
|
||||
|
||||
if fmt, ok := audioExtensions[ext]; ok {
|
||||
formatCounts[fmt]++
|
||||
formatSizes[fmt] += f.Length
|
||||
r.TrackNames = append(r.TrackNames, cleanTrackName(name))
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png":
|
||||
r.HasCoverArt = true
|
||||
case ".cue":
|
||||
r.HasCueSheet = true
|
||||
case ".log":
|
||||
r.HasRipLog = true
|
||||
}
|
||||
}
|
||||
|
||||
var dominantFormat release.AudioFormat
|
||||
var maxCount int
|
||||
for fmt, count := range formatCounts {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
dominantFormat = fmt
|
||||
}
|
||||
}
|
||||
r.Format = dominantFormat
|
||||
r.AudioFileCount = maxCount
|
||||
r.TotalAudioSize = formatSizes[dominantFormat]
|
||||
}
|
||||
|
||||
if r.HasRipLog {
|
||||
r.Source = release.SourceCD
|
||||
}
|
||||
|
||||
if r.TrackCount == 0 {
|
||||
r.TrackCount = r.AudioFileCount
|
||||
}
|
||||
|
||||
p.fillFromTitle(r, info.Name)
|
||||
|
||||
log.Trace().
|
||||
Str("hash", r.InfoHash).
|
||||
Str("format", r.Format.String()).
|
||||
Int("audio_files", r.AudioFileCount).
|
||||
Int64("audio_size", r.TotalAudioSize).
|
||||
Bool("cover", r.HasCoverArt).
|
||||
Bool("cue", r.HasCueSheet).
|
||||
Bool("log", r.HasRipLog).
|
||||
Msg("filled from torrent")
|
||||
}
|
||||
|
||||
func (p *GenericParser) fillFromTitle(r *release.Release, title string) {
|
||||
if title == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if m := bitratePattern.FindStringSubmatch(title); len(m) > 1 {
|
||||
r.Bitrate = m[1] + " kbps"
|
||||
}
|
||||
|
||||
for _, pattern := range hiResPatterns {
|
||||
m := pattern.FindStringSubmatch(title)
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
if r.BitDepth == 0 {
|
||||
if bd, err := strconv.Atoi(m[1]); err == nil {
|
||||
r.BitDepth = bd
|
||||
}
|
||||
}
|
||||
if len(m) > 2 && r.SampleRate == 0 {
|
||||
if sr, err := strconv.ParseFloat(m[2], 64); err == nil {
|
||||
r.SampleRate = int(sr * 1000)
|
||||
}
|
||||
}
|
||||
if r.BitDepth > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if m := sourcePattern.FindStringSubmatch(title); len(m) > 1 && r.Source == release.SourceUnknown {
|
||||
switch strings.ToUpper(m[1]) {
|
||||
case "CD":
|
||||
r.Source = release.SourceCD
|
||||
case "WEB":
|
||||
r.Source = release.SourceWEB
|
||||
case "VINYL", "LP":
|
||||
r.Source = release.SourceVinyl
|
||||
case "CASSETTE", "MC":
|
||||
r.Source = release.SourceCassette
|
||||
case "DVD":
|
||||
r.Source = release.SourceDVD
|
||||
case "BLU-RAY", "BLURAY":
|
||||
r.Source = release.SourceBluRay
|
||||
}
|
||||
}
|
||||
|
||||
if m := ripTypePattern.FindStringSubmatch(title); len(m) > 1 {
|
||||
r.RipType = m[1]
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Str("bitrate", r.Bitrate).
|
||||
Int("bit_depth", r.BitDepth).
|
||||
Int("sample_rate", r.SampleRate).
|
||||
Str("source", r.Source.String()).
|
||||
Str("rip_type", r.RipType).
|
||||
Msg("filled from title")
|
||||
}
|
||||
|
||||
var trackNumberPrefix = regexp.MustCompile(`^\d{1,3}[\s.\-]+`)
|
||||
|
||||
func cleanTrackName(name string) string {
|
||||
cleaned := trackNumberPrefix.ReplaceAllString(name, "")
|
||||
if cleaned == "" {
|
||||
return name
|
||||
}
|
||||
return strings.TrimSpace(cleaned)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
"homelab.lan/music-agregator/internal/release"
|
||||
)
|
||||
|
||||
func TestGenericParser_Parse(t *testing.T) {
|
||||
p := NewGenericParser()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
title string
|
||||
wantBitrate string
|
||||
wantBitDepth int
|
||||
wantSampleRate int
|
||||
wantSource release.Source
|
||||
wantRipType string
|
||||
}{
|
||||
{
|
||||
name: "discography no hires",
|
||||
title: "System Of A Down - Discography [FLAC Songs] [PMEDIA]",
|
||||
},
|
||||
{
|
||||
name: "hiphop hires 24-44",
|
||||
title: "Snoop Dogg - 10 Til' Midnight (2026 Hip Hop Rap) [Flac 24-44]",
|
||||
wantBitDepth: 24,
|
||||
wantSampleRate: 44000,
|
||||
},
|
||||
{
|
||||
name: "pop hires 24bit",
|
||||
title: "Sabrina Carpenter - Short n' Sweet [Deluxe] [2025] [Hi-Res FLAC 24bit]-Sc4r3cr0w",
|
||||
wantBitDepth: 24,
|
||||
},
|
||||
{
|
||||
name: "rock hires 24bit",
|
||||
title: "Linkin Park - From Zero [Deluxe Edition] [2025] [Hi-Res] [FLAC-24bit]-Sc4r3cr0w",
|
||||
},
|
||||
{
|
||||
name: "rock hires 24-48",
|
||||
title: "Linkin Park - From Zero (2024) [24Bit-48kHz] FLAC [PMEDIA]",
|
||||
wantBitDepth: 24,
|
||||
wantSampleRate: 48000,
|
||||
},
|
||||
{
|
||||
name: "hiphop hires 24-96",
|
||||
title: "J. Cole - The Fall-Off (2026 Hip Hop Rap) [Flac 24-96]",
|
||||
wantBitDepth: 24,
|
||||
wantSampleRate: 96000,
|
||||
},
|
||||
{
|
||||
name: "minimal format",
|
||||
title: "Bjork-Bastards.2012.FLAC-NewAlbumReleases",
|
||||
},
|
||||
{
|
||||
name: "vinyl hires",
|
||||
title: "Gorillaz - Demon Days [Live From The Apollo Theater] [2025] [Vinyl Hi-Res] [FLAC-24bit]-Sc4r3cr0w",
|
||||
},
|
||||
{
|
||||
name: "cd with log",
|
||||
title: "Linkin Park - Meteora (Tracks, Log, Cue, Scans) (2003) [FLAC] 88",
|
||||
},
|
||||
{
|
||||
name: "rock 16-44",
|
||||
title: "Heart - Jupiters Darling (2004 Rock) [Flac 16-44]",
|
||||
wantBitDepth: 16,
|
||||
wantSampleRate: 44000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := p.Parse(tt.title)
|
||||
|
||||
if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate {
|
||||
t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate)
|
||||
}
|
||||
if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth {
|
||||
t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth)
|
||||
}
|
||||
if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate {
|
||||
t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate)
|
||||
}
|
||||
if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource {
|
||||
t.Errorf("Source = %v, want %v", r.Source, tt.wantSource)
|
||||
}
|
||||
if tt.wantRipType != "" && r.RipType != tt.wantRipType {
|
||||
t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericParser_ParseTorrent(t *testing.T) {
|
||||
torrentData, err := os.ReadFile("/tmp/metallica.torrent")
|
||||
if err != nil {
|
||||
t.Skip("metallica.torrent not available")
|
||||
}
|
||||
|
||||
album := &metadataPb.Album{
|
||||
Title: "72 Seasons",
|
||||
AlbumType: "Album",
|
||||
ReleaseDate: "2023-04-14",
|
||||
TotalTracks: 12,
|
||||
TotalDiscs: 1,
|
||||
Artists: []*metadataPb.ArtistCredit{
|
||||
{Artist: &metadataPb.Artist{Name: "Metallica"}},
|
||||
},
|
||||
Genres: []*metadataPb.Genre{
|
||||
{Name: "Thrash Metal"},
|
||||
{Name: "Heavy Metal"},
|
||||
},
|
||||
Label: &metadataPb.Label{Name: "Blackened Recordings"},
|
||||
}
|
||||
|
||||
p := NewGenericParser()
|
||||
r := p.ParseTorrent(torrentData, album)
|
||||
|
||||
t.Logf("Artist: %s", r.Artist)
|
||||
t.Logf("Album: %s", r.Album)
|
||||
t.Logf("Year: %d", r.Year)
|
||||
t.Logf("Type: %s", r.Type)
|
||||
t.Logf("Genres: %v", r.Genres)
|
||||
t.Logf("Format: %s", r.Format)
|
||||
t.Logf("Source: %s", r.Source)
|
||||
t.Logf("Label: %s", r.Label)
|
||||
t.Logf("InfoHash: %s", r.InfoHash)
|
||||
t.Logf("TrackCount: %d", r.TrackCount)
|
||||
t.Logf("AudioFiles: %d", r.AudioFileCount)
|
||||
t.Logf("AudioSize: %d bytes", r.TotalAudioSize)
|
||||
t.Logf("HasCover: %v", r.HasCoverArt)
|
||||
t.Logf("HasCue: %v", r.HasCueSheet)
|
||||
t.Logf("HasLog: %v", r.HasRipLog)
|
||||
t.Logf("TrackNames: %v", r.TrackNames)
|
||||
t.Logf("Parsed OK: %v", r.ParsedSuccessfully)
|
||||
t.Logf("Errors: %v", r.ParseErrors)
|
||||
|
||||
if r.Artist != "Metallica" {
|
||||
t.Errorf("Artist = %q, want Metallica", r.Artist)
|
||||
}
|
||||
if r.Album != "72 Seasons" {
|
||||
t.Errorf("Album = %q, want 72 Seasons", r.Album)
|
||||
}
|
||||
if r.Year != 2023 {
|
||||
t.Errorf("Year = %d, want 2023", r.Year)
|
||||
}
|
||||
if r.Format != release.FormatFLAC {
|
||||
t.Errorf("Format = %v, want FLAC", r.Format)
|
||||
}
|
||||
if r.AudioFileCount != 12 {
|
||||
t.Errorf("AudioFileCount = %d, want 12", r.AudioFileCount)
|
||||
}
|
||||
if !r.HasCoverArt {
|
||||
t.Error("expected HasCoverArt")
|
||||
}
|
||||
if !r.HasCueSheet {
|
||||
t.Error("expected HasCueSheet")
|
||||
}
|
||||
if !r.HasRipLog {
|
||||
t.Error("expected HasRipLog")
|
||||
}
|
||||
if r.Source != release.SourceCD {
|
||||
t.Errorf("Source = %v, want CD (inferred from log)", r.Source)
|
||||
}
|
||||
if !r.ParsedSuccessfully {
|
||||
t.Errorf("ParsedSuccessfully = false, errors: %v", r.ParseErrors)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/bencode"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type MagnetResolver struct {
|
||||
client *torrent.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewMagnetResolver(timeout time.Duration) (*MagnetResolver, error) {
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.DataDir = ""
|
||||
cfg.NoDHT = false
|
||||
cfg.NoUpload = true
|
||||
cfg.Seed = false
|
||||
cfg.ListenPort = 0
|
||||
|
||||
client, err := torrent.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating torrent client: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Dur("timeout", timeout).Msg("magnet resolver initialized")
|
||||
|
||||
return &MagnetResolver{
|
||||
client: client,
|
||||
timeout: timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) {
|
||||
truncated := magnetURI
|
||||
if len(truncated) > 80 {
|
||||
truncated = truncated[:80] + "..."
|
||||
}
|
||||
log.Trace().Str("magnet", truncated).Msg("resolving magnet")
|
||||
|
||||
t, err := r.client.AddMagnet(magnetURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("adding magnet: %w", err)
|
||||
}
|
||||
defer t.Drop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
|
||||
defer cancel()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
noActiveSince := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.GotInfo():
|
||||
ticker.Stop()
|
||||
goto resolved
|
||||
case <-ctx.Done():
|
||||
stats := t.Stats()
|
||||
log.Warn().
|
||||
Str("hash", t.InfoHash().HexString()).
|
||||
Int("total_peers", stats.TotalPeers).
|
||||
Int("active_peers", stats.ActivePeers).
|
||||
Int("pending_peers", stats.PendingPeers).
|
||||
Int("half_open_peers", stats.HalfOpenPeers).
|
||||
Int("connected_seeders", stats.ConnectedSeeders).
|
||||
Msg("magnet resolve timed out")
|
||||
return nil, fmt.Errorf("timeout resolving magnet after %s: peers=%d active=%d seeders=%d",
|
||||
r.timeout, stats.TotalPeers, stats.ActivePeers, stats.ConnectedSeeders)
|
||||
case <-ticker.C:
|
||||
stats := t.Stats()
|
||||
log.Trace().
|
||||
Str("hash", t.InfoHash().HexString()).
|
||||
Int("total_peers", stats.TotalPeers).
|
||||
Int("active_peers", stats.ActivePeers).
|
||||
Int("connected_seeders", stats.ConnectedSeeders).
|
||||
Msg("magnet resolve waiting")
|
||||
|
||||
if stats.ActivePeers > 0 {
|
||||
noActiveSince = time.Now()
|
||||
}
|
||||
|
||||
if stats.TotalPeers > 0 && time.Since(noActiveSince) > 15*time.Second {
|
||||
log.Warn().
|
||||
Str("hash", t.InfoHash().HexString()).
|
||||
Int("total_peers", stats.TotalPeers).
|
||||
Int("active_peers", stats.ActivePeers).
|
||||
Msg("magnet has peers but none active for 15s, giving up early")
|
||||
return nil, fmt.Errorf("no active peers after 15s: total=%d active=%d", stats.TotalPeers, stats.ActivePeers)
|
||||
}
|
||||
}
|
||||
}
|
||||
resolved:
|
||||
|
||||
info := t.Info()
|
||||
log.Debug().
|
||||
Str("name", info.Name).
|
||||
Int("files", len(info.Files)).
|
||||
Int64("size", info.TotalLength()).
|
||||
Msg("magnet resolved")
|
||||
|
||||
mi := t.Metainfo()
|
||||
data, err := bencode.Marshal(metainfo.MetaInfo{
|
||||
InfoBytes: mi.InfoBytes,
|
||||
Announce: mi.Announce,
|
||||
AnnounceList: mi.AnnounceList,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling torrent data: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *MagnetResolver) Close() {
|
||||
r.client.Close()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
syntax = "proto3";
|
||||
package music_agregator.v1;
|
||||
option go_package = "homelab.lan/music-agregator/gen/music_agregator/v1/";
|
||||
|
||||
service MusicAgregatorService {
|
||||
rpc MonitorAlbum(MonitorAlbumRequest) returns (MonitorAlbumResponse) {}
|
||||
}
|
||||
|
||||
message MonitorAlbumRequest {
|
||||
string album_id = 1;
|
||||
IndexerOptions indexer_options = 2;
|
||||
QualityType quality = 3;
|
||||
}
|
||||
|
||||
message IndexerOptions {
|
||||
string tracker = 1;
|
||||
}
|
||||
|
||||
enum QualityType {
|
||||
QUALITY_UNSPECIFIED = 0;
|
||||
QUALITY_LOSSLESS = 1;
|
||||
QUALITY_LOSSY = 2;
|
||||
}
|
||||
|
||||
message MonitorAlbumResponse {
|
||||
MonitoredRelease release = 1;
|
||||
}
|
||||
|
||||
message MonitoredRelease {
|
||||
string info_hash = 1;
|
||||
string artist = 2;
|
||||
string album = 3;
|
||||
int32 year = 4;
|
||||
string format = 5;
|
||||
bool lossless = 6;
|
||||
int32 bit_depth = 7;
|
||||
int32 sample_rate = 8;
|
||||
string source = 9;
|
||||
int32 track_count = 10;
|
||||
repeated string track_names = 11;
|
||||
bool has_cover_art = 12;
|
||||
bool has_cue_sheet = 13;
|
||||
bool has_rip_log = 14;
|
||||
int64 total_audio_size = 15;
|
||||
string download_link = 16;
|
||||
int32 seeders = 17;
|
||||
string tracker = 18;
|
||||
}
|
||||
Reference in New Issue
Block a user