diff --git a/Dockerfile b/Dockerfile index 934f4c1..a3490ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-bookworm AS builder +FROM golang:1.24-bullseye AS builder WORKDIR /app @@ -10,24 +10,16 @@ RUN go mod download COPY . . -# Generate templ templates -RUN go run github.com/a-h/templ/cmd/templ@latest generate - # Build the server and the CLI tools RUN go build -o main_server ./cmd/server RUN go build -o create_user ./cmd/create-user -FROM debian:bookworm-slim +FROM debian:bullseye-slim WORKDIR /app -# Install ffmpeg, sqlite and ca-certificates -RUN apt-get update && apt-get install -y --no-install-recommends \ - ffmpeg \ - ca-certificates \ - sqlite3 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +# Required for sqlite +RUN apt-get update && apt-get install -y ca-certificates sqlite3 && rm -rf /var/lib/apt/lists/* # Create data directory for sqlite RUN mkdir -p /app/data diff --git a/cmd/create-user/main.go b/cmd/create-user/main.go index a49ae65..54ea94d 100644 --- a/cmd/create-user/main.go +++ b/cmd/create-user/main.go @@ -8,7 +8,7 @@ import ( "log" "os" - _ "modernc.org/sqlite" + _ "github.com/mattn/go-sqlite3" "mal/internal/database" "mal/internal/features/auth" @@ -30,7 +30,7 @@ func main() { dbFile = "mal.db" } - db, err := sql.Open("sqlite", dbFile) + db, err := sql.Open("sqlite3", dbFile) if err != nil { log.Fatalf("failed to open db: %v", err) } diff --git a/cmd/server/main.go b/cmd/server/main.go index f29e18b..f052b7f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,7 +6,7 @@ import ( "net/http" "os" - _ "modernc.org/sqlite" + _ "github.com/mattn/go-sqlite3" "mal/internal/database" "mal/internal/features/auth" @@ -21,7 +21,7 @@ func main() { dbFile = "mal.db" } - db, err := sql.Open("sqlite", dbFile) + db, err := sql.Open("sqlite3", dbFile) if err != nil { log.Fatalf("failed to open db: %v", err) } diff --git a/create-user b/create-user deleted file mode 100755 index decc463..0000000 Binary files a/create-user and /dev/null differ diff --git a/go.mod b/go.mod index 809b0d1..d829a25 100644 --- a/go.mod +++ b/go.mod @@ -1,98 +1,11 @@ module mal -go 1.25.0 +go 1.24.0 require ( github.com/a-h/templ v0.3.1001 - github.com/anacrolix/torrent v1.48.1-0.20230103142631-c20f73d53e9f github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 - golang.org/x/crypto v0.44.0 - modernc.org/sqlite v1.48.1 + github.com/mattn/go-sqlite3 v1.14.40 + golang.org/x/crypto v0.31.0 ) - -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/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/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/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // 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/gorilla/websocket v1.5.0 // indirect - github.com/huandu/xstrings v1.3.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect - github.com/mattn/go-isatty v0.0.20 // 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/ncruces/go-strftime v1.0.0 // 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/protolambda/ctxlock v0.1.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/tidwall/btree v1.8.1 // 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.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.14.0 // indirect - lukechampine.com/blake3 v1.1.6 // indirect - modernc.org/libc v1.70.0 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - zombiezen.com/go/sqlite v0.13.1 // indirect -) - -replace github.com/anacrolix/torrent => github.com/anacrolix/torrent v1.61.0 diff --git a/go.sum b/go.sum index b1e7b28..f2490e2 100644 --- a/go.sum +++ b/go.sum @@ -1,539 +1,12 @@ -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= -filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= -filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -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/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY= github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= -github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= -github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o= -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/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= -github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -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/lsan v0.1.0 h1:TbgB8fdVXgBwrNsJGHtht9+9FepNFu5H7dU8ek6XYAY= -github.com/anacrolix/lsan v0.1.0/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/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/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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -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/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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -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/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.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= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -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_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/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/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/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-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -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/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= -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/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -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/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -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/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -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= -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/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/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= -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/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.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/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= -modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= -modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= -modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= -modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= -modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o= -zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4= +github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94= +github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= diff --git a/internal/database/models.go b/internal/database/models.go index a8529b3..ef4861d 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -25,8 +25,6 @@ type Anime struct { TitleEnglish sql.NullString `json:"title_english"` TitleJapanese sql.NullString `json:"title_japanese"` Airing sql.NullBool `json:"airing"` - MagnetLink sql.NullString `json:"magnet_link"` - TorrentHash sql.NullString `json:"torrent_hash"` } type Session struct { diff --git a/internal/database/queries.sql b/internal/database/queries.sql index 142c299..e35f9f9 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -24,16 +24,14 @@ DELETE FROM session WHERE id = ?; DELETE FROM session WHERE user_id = ?; -- name: UpsertAnime :one -INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing, magnet_link, torrent_hash) -VALUES (?, ?, ?, ?, ?, ?, ?, ?) +INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing) +VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET title_original = excluded.title_original, title_english = excluded.title_english, title_japanese = excluded.title_japanese, image_url = excluded.image_url, - airing = excluded.airing, - magnet_link = excluded.magnet_link, - torrent_hash = excluded.torrent_hash + airing = excluded.airing RETURNING *; -- name: GetAnime :one @@ -58,9 +56,7 @@ SELECT a.title_english, a.title_japanese, a.image_url, - a.airing, - a.magnet_link, - a.torrent_hash + a.airing FROM watch_list_entry e JOIN anime a ON e.anime_id = a.id WHERE e.user_id = ? diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 61a9336..735d106 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -93,7 +93,7 @@ func (q *Queries) DeleteWatchListEntry(ctx context.Context, arg DeleteWatchListE } const getAnime = `-- name: GetAnime :one -SELECT id, title_original, image_url, created_at, title_english, title_japanese, airing, magnet_link, torrent_hash FROM anime WHERE id = ? LIMIT 1 +SELECT id, title_original, image_url, created_at, title_english, title_japanese, airing FROM anime WHERE id = ? LIMIT 1 ` func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) { @@ -107,8 +107,6 @@ func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) { &i.TitleEnglish, &i.TitleJapanese, &i.Airing, - &i.MagnetLink, - &i.TorrentHash, ) return i, err } @@ -168,9 +166,7 @@ SELECT a.title_english, a.title_japanese, a.image_url, - a.airing, - a.magnet_link, - a.torrent_hash + a.airing FROM watch_list_entry e JOIN anime a ON e.anime_id = a.id WHERE e.user_id = ? @@ -189,8 +185,6 @@ type GetUserWatchListRow struct { TitleJapanese sql.NullString `json:"title_japanese"` ImageUrl string `json:"image_url"` Airing sql.NullBool `json:"airing"` - MagnetLink sql.NullString `json:"magnet_link"` - TorrentHash sql.NullString `json:"torrent_hash"` } func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) { @@ -214,8 +208,6 @@ func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUse &i.TitleJapanese, &i.ImageUrl, &i.Airing, - &i.MagnetLink, - &i.TorrentHash, ); err != nil { return nil, err } @@ -255,17 +247,15 @@ func (q *Queries) GetWatchListEntry(ctx context.Context, arg GetWatchListEntryPa } const upsertAnime = `-- name: UpsertAnime :one -INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing, magnet_link, torrent_hash) -VALUES (?, ?, ?, ?, ?, ?, ?, ?) +INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing) +VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET title_original = excluded.title_original, title_english = excluded.title_english, title_japanese = excluded.title_japanese, image_url = excluded.image_url, - airing = excluded.airing, - magnet_link = excluded.magnet_link, - torrent_hash = excluded.torrent_hash -RETURNING id, title_original, image_url, created_at, title_english, title_japanese, airing, magnet_link, torrent_hash + airing = excluded.airing +RETURNING id, title_original, image_url, created_at, title_english, title_japanese, airing ` type UpsertAnimeParams struct { @@ -275,8 +265,6 @@ type UpsertAnimeParams struct { TitleJapanese sql.NullString `json:"title_japanese"` ImageUrl string `json:"image_url"` Airing sql.NullBool `json:"airing"` - MagnetLink sql.NullString `json:"magnet_link"` - TorrentHash sql.NullString `json:"torrent_hash"` } func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error) { @@ -287,8 +275,6 @@ func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime arg.TitleJapanese, arg.ImageUrl, arg.Airing, - arg.MagnetLink, - arg.TorrentHash, ) var i Anime err := row.Scan( @@ -299,8 +285,6 @@ func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime &i.TitleEnglish, &i.TitleJapanese, &i.Airing, - &i.MagnetLink, - &i.TorrentHash, ) return i, err } diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index f18df87..953eb03 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -109,7 +109,15 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { } func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") + path := r.URL.Path[len("/api/anime/"):] + idStr := "" + for i, c := range path { + if c == '/' { + idStr = path[:i] + break + } + } + id, _ := strconv.Atoi(idStr) if id <= 0 { http.Error(w, "invalid id", http.StatusBadRequest) @@ -164,29 +172,3 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } - -func (h *Handler) HandleAPIAnimeEpisodes(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, _ := strconv.Atoi(idStr) - if id <= 0 { - http.Error(w, "invalid id", http.StatusBadRequest) - return - } - - anime, err := h.svc.GetAnime(id) - if err != nil { - log.Printf("anime fetch error: %v", err) - http.Error(w, "Failed to fetch anime", http.StatusInternalServerError) - return - } - - episodes, err := h.svc.GetEpisodes(id, 1) - if err != nil { - log.Printf("episodes fetch error: %v", err) - // Return empty episodes instead of error - templates.EpisodesList(id, anime.Title, nil, anime.Episodes).Render(r.Context(), w) - return - } - - templates.EpisodesList(id, anime.Title, episodes.Episodes, anime.Episodes).Render(r.Context(), w) -} diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go index 829cf7f..e53d997 100644 --- a/internal/features/anime/service.go +++ b/internal/features/anime/service.go @@ -51,15 +51,3 @@ func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (j func (s *Service) GetRelations(id int) []jikan.RelationEntry { return s.jikanClient.GetFullRelations(id) } - -func (s *Service) GetEpisodes(id int, page int) (jikan.EpisodesResult, error) { - return s.jikanClient.GetEpisodes(id, page) -} - -func (s *Service) GetAllEpisodes(id int) ([]jikan.Episode, error) { - return s.jikanClient.GetAllEpisodes(id) -} - -func (s *Service) GetAnime(id int) (jikan.Anime, error) { - return s.jikanClient.GetAnimeByID(id) -} diff --git a/internal/jikan/client.go b/internal/jikan/client.go index 5dabfb7..41f9b69 100644 --- a/internal/jikan/client.go +++ b/internal/jikan/client.go @@ -16,7 +16,6 @@ type Client struct { topCache *expirable.LRU[int, TopAnimeResult] animeCache *expirable.LRU[int, Anime] relationsCache *expirable.LRU[int, JikanRelationsResponse] - episodesCache *expirable.LRU[string, EpisodesResult] } func NewClient() *Client { @@ -24,7 +23,6 @@ func NewClient() *Client { topCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1) animeCache := expirable.NewLRU[int, Anime](1000, nil, time.Hour*24) relationsCache := expirable.NewLRU[int, JikanRelationsResponse](1000, nil, time.Hour*24) - episodesCache := expirable.NewLRU[string, EpisodesResult](500, nil, time.Hour*6) return &Client{ httpClient: &http.Client{Timeout: 10 * time.Second}, @@ -33,7 +31,6 @@ func NewClient() *Client { topCache: topCache, animeCache: animeCache, relationsCache: relationsCache, - episodesCache: episodesCache, } } @@ -66,44 +63,3 @@ func (c *Client) fetchWithRetry(urlStr string, out interface{}) error { } return fmt.Errorf("max retries exceeded for %s", urlStr) } - -// GetEpisodes fetches episodes for an anime (paginated, 100 per page) -func (c *Client) GetEpisodes(animeID int, page int) (EpisodesResult, error) { - cacheKey := fmt.Sprintf("%d-%d", animeID, page) - if cached, ok := c.episodesCache.Get(cacheKey); ok { - return cached, nil - } - - url := fmt.Sprintf("%s/anime/%d/episodes?page=%d", c.baseURL, animeID, page) - var resp EpisodesResponse - if err := c.fetchWithRetry(url, &resp); err != nil { - return EpisodesResult{}, err - } - - result := EpisodesResult{ - Episodes: resp.Data, - HasNextPage: resp.Pagination.HasNextPage, - } - c.episodesCache.Add(cacheKey, result) - return result, nil -} - -// GetAllEpisodes fetches all episodes for an anime (handles pagination) -func (c *Client) GetAllEpisodes(animeID int) ([]Episode, error) { - var allEpisodes []Episode - page := 1 - - for { - result, err := c.GetEpisodes(animeID, page) - if err != nil { - return nil, err - } - allEpisodes = append(allEpisodes, result.Episodes...) - if !result.HasNextPage { - break - } - page++ - } - - return allEpisodes, nil -} diff --git a/internal/jikan/types.go b/internal/jikan/types.go index 25f8063..627ddf4 100644 --- a/internal/jikan/types.go +++ b/internal/jikan/types.go @@ -169,24 +169,3 @@ func (a Anime) DisplayTitle() string { } return a.Title } - -// Episode represents a single anime episode from Jikan API -type Episode struct { - MalID int `json:"mal_id"` - Title string `json:"title"` - TitleJP string `json:"title_japanese"` - TitleRom string `json:"title_romanji"` - Aired string `json:"aired"` - Filler bool `json:"filler"` - Recap bool `json:"recap"` -} - -type EpisodesResponse struct { - Data []Episode `json:"data"` - Pagination Pagination `json:"pagination"` -} - -type EpisodesResult struct { - Episodes []Episode - HasNextPage bool -} diff --git a/internal/nyaa/client.go b/internal/nyaa/client.go deleted file mode 100644 index a1e2899..0000000 --- a/internal/nyaa/client.go +++ /dev/null @@ -1,352 +0,0 @@ -package nyaa - -import ( - "encoding/xml" - "fmt" - "io" - "net/http" - "net/url" - "regexp" - "strconv" - "strings" - "time" - - "github.com/hashicorp/golang-lru/v2/expirable" -) - -type Client struct { - httpClient *http.Client - baseURL string - cache *expirable.LRU[string, []Torrent] - magnetCache *expirable.LRU[string, string] -} - -type Torrent struct { - Title string `json:"title"` - Magnet string `json:"magnet"` - Size string `json:"size"` - Seeders int `json:"seeders"` - Leechers int `json:"leechers"` - Date string `json:"date"` - Episode int `json:"episode"` - ViewURL string `json:"view_url"` -} - -// RSS feed structures -type rssResponse struct { - XMLName xml.Name `xml:"rss"` - Channel rssChannel `xml:"channel"` -} - -type rssChannel struct { - Items []rssItem `xml:"item"` -} - -type rssItem struct { - Title string `xml:"title"` - Link string `xml:"link"` - GUID string `xml:"guid"` - PubDate string `xml:"pubDate"` - Seeders string `xml:"seeders"` - Leechers string `xml:"leechers"` - Size string `xml:"size"` -} - -func NewClient() *Client { - cache := expirable.NewLRU[string, []Torrent](200, nil, time.Minute*15) - magnetCache := expirable.NewLRU[string, string](500, nil, time.Hour*24) - return &Client{ - httpClient: &http.Client{Timeout: 15 * time.Second}, - baseURL: "https://nyaa.si", - cache: cache, - magnetCache: magnetCache, - } -} - -// SearchAnime searches for anime torrents on nyaa.si -// category 1_2 = Anime - English-translated -func (c *Client) SearchAnime(query string) ([]Torrent, error) { - if cached, ok := c.cache.Get(query); ok { - return cached, nil - } - - // Build search URL with RSS feed - params := url.Values{} - params.Set("f", "0") // filter: no filter - params.Set("c", "1_2") // category: Anime - English-translated - params.Set("q", query) - params.Set("s", "seeders") // sort by seeders - params.Set("o", "desc") // descending order - params.Set("page", "rss") // RSS format - - reqURL := fmt.Sprintf("%s/?%s", c.baseURL, params.Encode()) - - resp, err := c.httpClient.Get(reqURL) - if err != nil { - return nil, fmt.Errorf("nyaa request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("nyaa returned status %d", resp.StatusCode) - } - - var rss rssResponse - if err := xml.NewDecoder(resp.Body).Decode(&rss); err != nil { - return nil, fmt.Errorf("failed to parse nyaa rss: %w", err) - } - - torrents := make([]Torrent, 0, len(rss.Channel.Items)) - for _, item := range rss.Channel.Items { - seeders, _ := strconv.Atoi(item.Seeders) - leechers, _ := strconv.Atoi(item.Leechers) - - // Skip dead torrents (0 seeders) - if seeders == 0 { - continue - } - - // Extract torrent ID from download link - // Link format: https://nyaa.si/download/1234567.torrent - viewURL := extractViewURL(item.Link) - - t := Torrent{ - Title: item.Title, - Magnet: "", // Will be fetched on demand - Size: item.Size, - Seeders: seeders, - Leechers: leechers, - Date: item.PubDate, - Episode: parseEpisodeNumber(item.Title), - ViewURL: viewURL, - } - - // Check if GUID is already a magnet link - if strings.HasPrefix(item.GUID, "magnet:") { - t.Magnet = item.GUID - } - - torrents = append(torrents, t) - } - - // Fetch magnets for top results (limit to avoid rate limiting) - for i := range torrents { - if i >= 20 { - break - } - if torrents[i].Magnet == "" && torrents[i].ViewURL != "" { - magnet, err := c.fetchMagnet(torrents[i].ViewURL) - if err == nil { - torrents[i].Magnet = magnet - } - } - } - - c.cache.Add(query, torrents) - return torrents, nil -} - -// fetchMagnet scrapes the nyaa view page to get the magnet link -func (c *Client) fetchMagnet(viewURL string) (string, error) { - if cached, ok := c.magnetCache.Get(viewURL); ok { - return cached, nil - } - - resp, err := c.httpClient.Get(viewURL) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("nyaa returned status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // Find magnet link in page - // Pattern: href="magnet:?xt=urn:btih:..." - magnetRe := regexp.MustCompile(`href="(magnet:\?xt=urn:btih:[^"]+)"`) - matches := magnetRe.FindSubmatch(body) - if len(matches) < 2 { - return "", fmt.Errorf("magnet link not found") - } - - magnet := string(matches[1]) - c.magnetCache.Add(viewURL, magnet) - return magnet, nil -} - -// extractViewURL converts download URL to view URL -func extractViewURL(downloadURL string) string { - // https://nyaa.si/download/1234567.torrent -> https://nyaa.si/view/1234567 - re := regexp.MustCompile(`/download/(\d+)\.torrent`) - matches := re.FindStringSubmatch(downloadURL) - if len(matches) < 2 { - return "" - } - return fmt.Sprintf("https://nyaa.si/view/%s", matches[1]) -} - -// SearchEpisode searches for a specific episode of an anime -func (c *Client) SearchEpisode(animeTitle string, episode int) ([]Torrent, error) { - // Format episode number with leading zero for single digits - epStr := fmt.Sprintf("%02d", episode) - query := fmt.Sprintf("%s %s", animeTitle, epStr) - - torrents, err := c.SearchAnime(query) - if err != nil { - return nil, err - } - - // Strict filter: only return torrents that exactly match the episode - var filtered []Torrent - for _, t := range torrents { - parsedEp := t.Episode - if parsedEp == 0 { - // Re-parse to be sure - parsedEp = parseEpisodeNumber(t.Title) - } - if parsedEp == episode { - filtered = append(filtered, t) - } - } - - // If strict filtering returns nothing, try a more specific search - if len(filtered) == 0 { - // Try with episode marker formats - altQueries := []string{ - fmt.Sprintf("%s E%02d", animeTitle, episode), - fmt.Sprintf("%s - %02d", animeTitle, episode), - fmt.Sprintf("%s Episode %d", animeTitle, episode), - } - - for _, altQuery := range altQueries { - altTorrents, err := c.SearchAnime(altQuery) - if err != nil { - continue - } - for _, t := range altTorrents { - parsedEp := parseEpisodeNumber(t.Title) - if parsedEp == episode { - filtered = append(filtered, t) - } - } - if len(filtered) > 0 { - break - } - } - } - - // Deduplicate by magnet link - seen := make(map[string]bool) - var deduped []Torrent - for _, t := range filtered { - if t.Magnet != "" && !seen[t.Magnet] { - seen[t.Magnet] = true - deduped = append(deduped, t) - } else if t.Magnet == "" && !seen[t.ViewURL] { - seen[t.ViewURL] = true - deduped = append(deduped, t) - } - } - - return deduped, nil -} - -// parseEpisodeNumber tries to extract episode number from torrent title -func parseEpisodeNumber(title string) int { - // First, check if this is a batch/complete series (should return 0) - batchPatterns := []string{ - `(?i)\b(batch|complete|全話|全\d+話)\b`, - `(?i)\b(\d{1,4})\s*[-~]\s*(\d{1,4})\b`, // Range like 01-12 or 01~24 - } - for _, p := range batchPatterns { - if regexp.MustCompile(p).MatchString(title) { - return 0 - } - } - - // Patterns ordered by specificity (most specific first) - patterns := []*regexp.Regexp{ - // S01E01 format (most specific) - regexp.MustCompile(`(?i)S\d{1,2}E(\d{1,4})(?:v\d)?(?:\s|\[|\]|$)`), - // Episode 01 format - regexp.MustCompile(`(?i)Episode\s*(\d{1,4})(?:v\d)?(?:\s|\[|\]|$)`), - // - 01 format (common in fansubs like [SubGroup] Anime - 01) - regexp.MustCompile(`[-–]\s*(\d{1,4})(?:v\d)?(?:\s|\[|\]|$)`), - // E01 format (standalone) - regexp.MustCompile(`(?i)\bE(\d{1,4})(?:v\d)?(?:\s|\[|\]|$)`), - // #01 format - regexp.MustCompile(`#(\d{1,4})(?:v\d)?(?:\s|\[|\]|$)`), - } - - for _, re := range patterns { - if matches := re.FindStringSubmatch(title); len(matches) > 1 { - ep, err := strconv.Atoi(matches[1]) - if err != nil { - continue - } - // Sanity check: episode numbers are typically < 2000 - // This filters out years (2024) and resolutions (1920) - if ep > 0 && ep < 2000 { - return ep - } - } - } - - return 0 -} - -// FilterByQuality returns torrents matching the quality preference -func FilterByQuality(torrents []Torrent, quality string) []Torrent { - if quality == "" { - return torrents - } - - var filtered []Torrent - qualityLower := strings.ToLower(quality) - - for _, t := range torrents { - titleLower := strings.ToLower(t.Title) - if strings.Contains(titleLower, qualityLower) { - filtered = append(filtered, t) - } - } - - if len(filtered) == 0 { - return torrents - } - return filtered -} - -// BestTorrent returns the torrent with the most seeders that has a magnet -func BestTorrent(torrents []Torrent) *Torrent { - if len(torrents) == 0 { - return nil - } - - var best *Torrent - for i := range torrents { - if torrents[i].Magnet == "" { - continue - } - if best == nil || torrents[i].Seeders > best.Seeders { - best = &torrents[i] - } - } - - // Fallback to first with magnet - if best == nil { - for i := range torrents { - if torrents[i].Magnet != "" { - return &torrents[i] - } - } - } - - return best -} diff --git a/internal/nyaa/client_test.go b/internal/nyaa/client_test.go deleted file mode 100644 index 67cabe8..0000000 --- a/internal/nyaa/client_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package nyaa - -import "testing" - -func TestParseEpisodeNumber(t *testing.T) { - tests := []struct { - title string - expected int - }{ - // Standard fansub formats - {"[SubsPlease] Naruto - 01 (1080p) [ABC123].mkv", 1}, - {"[SubsPlease] Naruto - 220 (1080p) [ABC123].mkv", 220}, - {"[Erai-raws] One Piece - 1100 [1080p][Multiple Subtitle].mkv", 1100}, - - // S01E01 format - {"Naruto S01E01 1080p WEB-DL", 1}, - {"Attack on Titan S04E28 Final", 28}, - - // Episode keyword - {"Naruto Episode 1 [1080p]", 1}, - {"One Piece Episode 1089", 1089}, - - // E01 format - {"Naruto E01 [1080p]", 1}, - {"Bleach E366 Final", 366}, - - // Hash/number format - {"Anime Title #42 [720p]", 42}, - - // Should NOT match these (batches/complete) - {"[SubsPlease] Naruto (01-220) [Batch]", 0}, - {"Naruto Complete Series 1-220", 0}, - {"Naruto Batch 01~220", 0}, - {"[Erai-raws] Death Note - 01 ~ 37 [1080p][BATCH][Multiple Subtitle]", 0}, - {"[SubGroup] Anime - 01-12 [BD][1080p]", 0}, - {"Show Name 01 - 24 Complete", 0}, - - // Should NOT match resolutions/years as episodes - {"Naruto The Movie 2024 [1080p]", 0}, - {"[1920x1080] Naruto Remastered", 0}, - - // Version numbers should be stripped - {"[SubsPlease] Naruto - 05v2 (1080p)", 5}, - {"Bleach - 100v3 [720p]", 100}, - } - - for _, tt := range tests { - t.Run(tt.title, func(t *testing.T) { - got := parseEpisodeNumber(tt.title) - if got != tt.expected { - t.Errorf("parseEpisodeNumber(%q) = %d, want %d", tt.title, got, tt.expected) - } - }) - } -} diff --git a/internal/server/routes.go b/internal/server/routes.go index 1856f88..d7adfa1 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -1,9 +1,7 @@ package server import ( - "log/slog" "net/http" - "os" "mal/internal/database" "mal/internal/features/anime" @@ -11,7 +9,6 @@ import ( "mal/internal/features/watchlist" "mal/internal/jikan" "mal/internal/shared/middleware" - "mal/internal/streaming" ) type Config struct { @@ -31,13 +28,6 @@ func NewRouter(cfg Config) http.Handler { animeSvc := anime.NewService(cfg.JikanClient, cfg.DB) animeHandler := anime.NewHandler(animeSvc) - logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) - streamSvc, err := streaming.NewService(logger) - if err != nil { - panic("failed to initialize streaming service: " + err.Error()) - } - streamHandler := streaming.NewHandler(streamSvc, cfg.JikanClient) - // Serve static files fs := http.FileServer(http.Dir("./static")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) @@ -49,8 +39,7 @@ func NewRouter(cfg Config) http.Handler { mux.HandleFunc("/api/search-quick", animeHandler.HandleQuickSearch) mux.HandleFunc("/api/catalog", animeHandler.HandleAPICatalog) mux.HandleFunc("/anime/", animeHandler.HandleAnimeDetails) - mux.HandleFunc("/api/anime/{id}/relations", animeHandler.HandleAPIAnimeRelations) - mux.HandleFunc("/api/anime/{id}/episodes", animeHandler.HandleAPIAnimeEpisodes) + mux.HandleFunc("/api/anime/", animeHandler.HandleAPIAnimeRelations) // Auth Endpoints mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { @@ -62,9 +51,6 @@ func NewRouter(cfg Config) http.Handler { }) mux.HandleFunc("/logout", authHandler.HandleLogout) - // Streaming endpoints - streamHandler.RegisterRoutes(mux) - // Watchlist POST endpoint (Protected) mux.Handle("/api/watchlist/export", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleExportWatchlist))) mux.Handle("/api/watchlist/import", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleImportWatchlist))) diff --git a/internal/shared/middleware/auth.go b/internal/shared/middleware/auth.go index 20c349a..567d25d 100644 --- a/internal/shared/middleware/auth.go +++ b/internal/shared/middleware/auth.go @@ -61,12 +61,10 @@ func RequireAuth(next http.Handler) http.Handler { // RequireGlobalAuth ensures that a valid user is in the context for all routes except login and static func RequireGlobalAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Allow unauthenticated access to login, search, static files, and streaming + // Allow unauthenticated access to login, search, and static files if r.URL.Path == "/login" || strings.HasPrefix(r.URL.Path, "/static/") || r.URL.Path == "/search" || r.URL.Path == "/api/search" || r.URL.Path == "/api/search-quick" || - r.URL.Path == "/" || - strings.HasPrefix(r.URL.Path, "/watch/") || strings.HasPrefix(r.URL.Path, "/api/stream/") || - strings.HasPrefix(r.URL.Path, "/anime/") || strings.HasPrefix(r.URL.Path, "/api/anime/") { + r.URL.Path == "/" { next.ServeHTTP(w, r) return } diff --git a/internal/streaming/handler.go b/internal/streaming/handler.go deleted file mode 100644 index a6b35f3..0000000 --- a/internal/streaming/handler.go +++ /dev/null @@ -1,373 +0,0 @@ -package streaming - -import ( - "encoding/json" - "fmt" - "html" - "log" - "net/http" - "strconv" - - "mal/internal/jikan" - "mal/internal/nyaa" - "mal/internal/templates" -) - -type Handler struct { - svc *Service - jikan *jikan.Client -} - -func NewHandler(svc *Service, jikanClient *jikan.Client) *Handler { - return &Handler{svc: svc, jikan: jikanClient} -} - -func (h *Handler) RegisterRoutes(mux *http.ServeMux) { - // Watch page - mux.HandleFunc("GET /watch/{animeID}/{episode}", h.HandleWatchPage) - - // Search endpoints - mux.HandleFunc("GET /api/stream/search", h.HandleSearch) - mux.HandleFunc("GET /api/stream/search/episode", h.HandleSearchEpisode) - mux.HandleFunc("GET /api/stream/search-htmx", h.HandleSearchHTMX) - - // Streaming endpoints - mux.HandleFunc("POST /api/stream/start", h.HandleStartStream) - mux.HandleFunc("GET /api/stream/video/{hash}", h.HandleStreamVideo) - mux.HandleFunc("GET /api/stream/info/{hash}", h.HandleStreamInfo) - mux.HandleFunc("DELETE /api/stream/{hash}", h.HandleDropStream) - - // HLS endpoints - mux.HandleFunc("POST /api/stream/hls/{hash}", h.HandleStartHLS) - mux.HandleFunc("GET /api/stream/hls/{hash}/playlist.m3u8", h.HandleHLSPlaylist) - mux.HandleFunc("GET /api/stream/hls/{hash}/{segment}", h.HandleHLSSegment) -} - -// HandleWatchPage renders the video player page for an episode -func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) { - animeIDStr := r.PathValue("animeID") - episodeStr := r.PathValue("episode") - - animeID, err := strconv.Atoi(animeIDStr) - if err != nil || animeID <= 0 { - http.NotFound(w, r) - return - } - - episode, err := strconv.Atoi(episodeStr) - if err != nil || episode <= 0 { - http.NotFound(w, r) - return - } - - anime, err := h.jikan.GetAnimeByID(animeID) - if err != nil { - log.Printf("failed to get anime %d: %v", animeID, err) - http.Error(w, "Failed to fetch anime", http.StatusInternalServerError) - return - } - - // Build list of title variations to try - // Fansubs typically use English titles or romaji - var titles []string - if anime.TitleEnglish != "" { - titles = append(titles, anime.TitleEnglish) - } - titles = append(titles, anime.Title) // Usually romaji - titles = append(titles, anime.TitleSynonyms...) - - // Search using title variations until we find results - var torrents []nyaa.Torrent - for _, title := range titles { - torrents, err = h.svc.SearchEpisode(title, episode) - if err != nil { - log.Printf("torrent search error for %q: %v", title, err) - continue - } - if len(torrents) > 0 { - break - } - } - - // Filter to 1080p by default, fallback to all - filtered := nyaa.FilterByQuality(torrents, "1080p") - if len(filtered) == 0 { - filtered = torrents - } - - // Limit to top 10 - if len(filtered) > 10 { - filtered = filtered[:10] - } - - templates.WatchPage(anime, episode, filtered).Render(r.Context(), w) -} - -// HandleSearch searches nyaa for anime torrents -func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("q") - if query == "" { - http.Error(w, "query required", http.StatusBadRequest) - return - } - - torrents, err := h.svc.SearchAnime(query) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - quality := r.URL.Query().Get("quality") - if quality != "" { - torrents = nyaa.FilterByQuality(torrents, quality) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(torrents) -} - -// HandleSearchEpisode searches nyaa for a specific episode -func (h *Handler) HandleSearchEpisode(w http.ResponseWriter, r *http.Request) { - title := r.URL.Query().Get("title") - episodeStr := r.URL.Query().Get("episode") - - if title == "" || episodeStr == "" { - http.Error(w, "title and episode required", http.StatusBadRequest) - return - } - - episode, err := strconv.Atoi(episodeStr) - if err != nil { - http.Error(w, "invalid episode number", http.StatusBadRequest) - return - } - - torrents, err := h.svc.SearchEpisode(title, episode) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - quality := r.URL.Query().Get("quality") - if quality != "" { - torrents = nyaa.FilterByQuality(torrents, quality) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(torrents) -} - -type startStreamRequest struct { - Magnet string `json:"magnet"` -} - -type startStreamResponse struct { - InfoHash string `json:"info_hash"` - Name string `json:"name"` - Size int64 `json:"size"` -} - -// HandleStartStream starts streaming a torrent from a magnet link -func (h *Handler) HandleStartStream(w http.ResponseWriter, r *http.Request) { - var req startStreamRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - if req.Magnet == "" { - http.Error(w, "magnet required", http.StatusBadRequest) - return - } - - info, err := h.svc.AddMagnet(r.Context(), req.Magnet) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(startStreamResponse{ - InfoHash: info.InfoHash, - Name: info.Name, - Size: info.Size, - }) -} - -// HandleStreamVideo streams the video file from a torrent -func (h *Handler) HandleStreamVideo(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - if hash == "" { - http.Error(w, "hash required", http.StatusBadRequest) - return - } - - if err := h.svc.StreamVideo(w, r, hash); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -// HandleStreamInfo returns info about an active stream -func (h *Handler) HandleStreamInfo(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - if hash == "" { - http.Error(w, "hash required", http.StatusBadRequest) - return - } - - info, err := h.svc.GetStreamInfo(hash) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(info) -} - -// HandleDropStream stops and removes a stream -func (h *Handler) HandleDropStream(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - if hash == "" { - http.Error(w, "hash required", http.StatusBadRequest) - return - } - - h.svc.DropTorrent(hash) - w.WriteHeader(http.StatusNoContent) -} - -// TorrentResult for HTMX responses -type TorrentResult struct { - Torrents []nyaa.Torrent - Query string - Episode int -} - -// HandleSearchHTMX returns HTML for torrent search results -func (h *Handler) HandleSearchHTMX(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("q") - if query == "" { - fmt.Fprint(w, `
enter a search query
`) - return - } - - torrents, err := h.svc.SearchAnime(query) - if err != nil { - fmt.Fprintf(w, `
error: %s
`, html.EscapeString(err.Error())) - return - } - - quality := r.URL.Query().Get("quality") - if quality != "" { - torrents = nyaa.FilterByQuality(torrents, quality) - } - - // Filter to only torrents with magnet links - var withMagnets []nyaa.Torrent - for _, t := range torrents { - if t.Magnet != "" { - withMagnets = append(withMagnets, t) - } - } - - if len(withMagnets) == 0 { - fmt.Fprint(w, `
no torrents found (or no magnet links available)
`) - return - } - - // Return simple HTML list - fmt.Fprint(w, `
`) - for _, t := range withMagnets { - fmt.Fprintf(w, ` -
-
%s
-
- %s - %d seeds -
-
- `, html.EscapeString(t.Magnet), html.EscapeString(t.Title), html.EscapeString(t.Size), t.Seeders) - } - fmt.Fprint(w, `
`) -} - -// HandleStartHLS starts HLS transcoding for a torrent -func (h *Handler) HandleStartHLS(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - if hash == "" { - http.Error(w, "hash required", http.StatusBadRequest) - return - } - - epStr := r.URL.Query().Get("ep") - episode, _ := strconv.Atoi(epStr) - - // Check torrent exists - if _, ok := h.svc.GetTorrent(hash); !ok { - http.Error(w, "torrent not found - start stream first", http.StatusNotFound) - return - } - - session, err := h.svc.StartHLS(r.Context(), hash, episode) - if err != nil { - log.Printf("HLS start error: %v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Use sessionKey for subsequent requests - sessionKey := hash - if episode > 0 { - sessionKey = fmt.Sprintf("%s-ep%d", hash, episode) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "playlist": fmt.Sprintf("/api/stream/hls/%s/playlist.m3u8", sessionKey), - "status": "ready", - "output": session.OutputDir, - }) -} - -// HandleHLSPlaylist serves the HLS playlist file -func (h *Handler) HandleHLSPlaylist(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - if hash == "" { - http.Error(w, "hash required", http.StatusBadRequest) - return - } - - session, ok := h.svc.GetHLSSession(hash) - if !ok { - http.Error(w, "HLS session not found - start transcoding first", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") - w.Header().Set("Cache-Control", "no-cache") - http.ServeFile(w, r, session.Playlist) -} - -// HandleHLSSegment serves an HLS segment file -func (h *Handler) HandleHLSSegment(w http.ResponseWriter, r *http.Request) { - hash := r.PathValue("hash") - segment := r.PathValue("segment") - - if hash == "" || segment == "" { - http.Error(w, "hash and segment required", http.StatusBadRequest) - return - } - - session, ok := h.svc.GetHLSSession(hash) - if !ok { - http.Error(w, "HLS session not found", http.StatusNotFound) - return - } - - // Serve the segment file - segmentPath := fmt.Sprintf("%s/%s", session.OutputDir, segment) - w.Header().Set("Content-Type", "video/mp2t") - http.ServeFile(w, r, segmentPath) -} diff --git a/internal/streaming/hls.go b/internal/streaming/hls.go deleted file mode 100644 index 016239e..0000000 --- a/internal/streaming/hls.go +++ /dev/null @@ -1,248 +0,0 @@ -package streaming - -import ( - "context" - "fmt" - "io" - "log/slog" - "os" - "os/exec" - "path/filepath" - "sync" - "time" -) - -// HLSTranscoder manages on-demand HLS transcoding sessions -type HLSTranscoder struct { - logger *slog.Logger - sessions map[string]*HLSSession - mu sync.RWMutex - baseDir string -} - -// HLSSession represents an active transcoding session -type HLSSession struct { - InfoHash string - OutputDir string - Playlist string - cmd *exec.Cmd - cancel context.CancelFunc - ready chan struct{} - err error - lastAccess time.Time - mu sync.Mutex -} - -// NewHLSTranscoder creates a new HLS transcoder -func NewHLSTranscoder(logger *slog.Logger) (*HLSTranscoder, error) { - // Check ffmpeg is available - if _, err := exec.LookPath("ffmpeg"); err != nil { - return nil, fmt.Errorf("ffmpeg not found in PATH: %w", err) - } - - baseDir := filepath.Join(os.TempDir(), "mal-hls") - if err := os.MkdirAll(baseDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create HLS temp dir: %w", err) - } - - t := &HLSTranscoder{ - logger: logger, - sessions: make(map[string]*HLSSession), - baseDir: baseDir, - } - - // Start cleanup goroutine - go t.cleanupLoop() - - return t, nil -} - -// StartSessionWithReader starts transcoding from a reader (piped input) to HLS -func (t *HLSTranscoder) StartSessionWithReader(infoHash string, reader io.Reader) (*HLSSession, error) { - t.mu.Lock() - defer t.mu.Unlock() - - // Return existing session if available - if session, ok := t.sessions[infoHash]; ok { - session.mu.Lock() - session.lastAccess = time.Now() - session.mu.Unlock() - return session, nil - } - - // Create output directory - outputDir := filepath.Join(t.baseDir, infoHash) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create output dir: %w", err) - } - - playlist := filepath.Join(outputDir, "stream.m3u8") - - ctx, cancel := context.WithCancel(context.Background()) - - session := &HLSSession{ - InfoHash: infoHash, - OutputDir: outputDir, - Playlist: playlist, - cancel: cancel, - ready: make(chan struct{}), - lastAccess: time.Now(), - } - - // Start ffmpeg with pipe input - // Use browser-compatible H.264 baseline/main profile and AAC-LC - cmd := exec.CommandContext(ctx, "ffmpeg", - "-i", "pipe:0", // Read from stdin - // Video: H.264 with browser-compatible settings - "-c:v", "libx264", - "-preset", "veryfast", - "-crf", "23", - "-profile:v", "main", - "-level", "4.0", - "-pix_fmt", "yuv420p", - // Audio: AAC-LC stereo - "-c:a", "aac", - "-b:a", "128k", - "-ac", "2", - "-ar", "44100", - // HLS output - "-f", "hls", - "-hls_time", "4", - "-hls_list_size", "0", - "-hls_flags", "append_list+independent_segments", - "-hls_segment_type", "mpegts", - "-hls_segment_filename", filepath.Join(outputDir, "segment_%03d.ts"), - "-start_number", "0", - playlist, - ) - - // Pipe the reader to ffmpeg's stdin - cmd.Stdin = reader - session.cmd = cmd - - // Capture stderr for debugging - cmd.Stderr = &ffmpegLogger{logger: t.logger, infoHash: infoHash} - - if err := cmd.Start(); err != nil { - cancel() - os.RemoveAll(outputDir) - return nil, fmt.Errorf("failed to start ffmpeg: %w", err) - } - - t.sessions[infoHash] = session - - // Wait for playlist to be created - go func() { - defer close(session.ready) - - for i := 0; i < 120; i++ { // Wait up to 60 seconds - if _, err := os.Stat(playlist); err == nil { - // Check if at least one segment exists - segments, _ := filepath.Glob(filepath.Join(outputDir, "segment_*.ts")) - if len(segments) > 0 { - t.logger.Info("HLS segments ready", "hash", infoHash, "segments", len(segments)) - return - } - } - time.Sleep(500 * time.Millisecond) - } - - session.err = fmt.Errorf("timeout waiting for HLS segments") - cancel() - }() - - // Monitor process - go func() { - err := cmd.Wait() - if err != nil && ctx.Err() == nil { - t.logger.Error("ffmpeg exited with error", "hash", infoHash, "error", err) - } else { - t.logger.Info("ffmpeg finished", "hash", infoHash) - } - }() - - return session, nil -} - -// GetSession returns an existing session -func (t *HLSTranscoder) GetSession(infoHash string) (*HLSSession, bool) { - t.mu.RLock() - defer t.mu.RUnlock() - - session, ok := t.sessions[infoHash] - if ok { - session.mu.Lock() - session.lastAccess = time.Now() - session.mu.Unlock() - } - return session, ok -} - -// StopSession stops a transcoding session -func (t *HLSTranscoder) StopSession(infoHash string) { - t.mu.Lock() - defer t.mu.Unlock() - - if session, ok := t.sessions[infoHash]; ok { - session.cancel() - os.RemoveAll(session.OutputDir) - delete(t.sessions, infoHash) - } -} - -// WaitReady waits for the session to have segments ready -func (s *HLSSession) WaitReady(ctx context.Context) error { - select { - case <-s.ready: - return s.err - case <-ctx.Done(): - return ctx.Err() - } -} - -// cleanupLoop removes stale sessions -func (t *HLSTranscoder) cleanupLoop() { - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - - for range ticker.C { - t.mu.Lock() - now := time.Now() - for hash, session := range t.sessions { - session.mu.Lock() - if now.Sub(session.lastAccess) > 10*time.Minute { - session.cancel() - os.RemoveAll(session.OutputDir) - delete(t.sessions, hash) - t.logger.Info("cleaned up stale HLS session", "hash", hash) - } - session.mu.Unlock() - } - t.mu.Unlock() - } -} - -// Shutdown stops all sessions -func (t *HLSTranscoder) Shutdown() { - t.mu.Lock() - defer t.mu.Unlock() - - for hash, session := range t.sessions { - session.cancel() - os.RemoveAll(session.OutputDir) - delete(t.sessions, hash) - } - - os.RemoveAll(t.baseDir) -} - -// ffmpegLogger logs ffmpeg stderr output -type ffmpegLogger struct { - logger *slog.Logger - infoHash string -} - -func (l *ffmpegLogger) Write(p []byte) (n int, err error) { - l.logger.Debug("ffmpeg", "hash", l.infoHash, "output", string(p)) - return len(p), nil -} diff --git a/internal/streaming/service.go b/internal/streaming/service.go deleted file mode 100644 index c733586..0000000 --- a/internal/streaming/service.go +++ /dev/null @@ -1,504 +0,0 @@ -package streaming - -import ( - "context" - "fmt" - "log/slog" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "golang.org/x/net/proxy" - - "mal/internal/nyaa" -) - -type Service struct { - client *torrent.Client - nyaa *nyaa.Client - hls *HLSTranscoder - mu sync.RWMutex - activeTorrents map[string]*torrent.Torrent - logger *slog.Logger -} - -type StreamInfo struct { - InfoHash string - Name string - Size int64 - Files []FileInfo - Progress float64 - DownloadRate int64 - Peers int -} - -type FileInfo struct { - Index int - Path string - Size int64 -} - -func NewService(logger *slog.Logger) (*Service, error) { - cfg := torrent.NewDefaultClientConfig() - cfg.ListenPort = 42069 - cfg.Seed = false - cfg.NoUpload = true - - // Use temp directory for downloads - cfg.DataDir = filepath.Join("/tmp", "mal-streams") - - // Configure SOCKS5 proxy if TORRENT_PROXY is set - // Usage: export TORRENT_PROXY=socks5://127.0.0.1:1080 - // Start with: ssh -D 1080 -N user@your-vps - if proxyURL := os.Getenv("TORRENT_PROXY"); proxyURL != "" { - parsed, err := url.Parse(proxyURL) - if err != nil { - return nil, fmt.Errorf("invalid TORRENT_PROXY url: %w", err) - } - - dialer, err := proxy.FromURL(parsed, proxy.Direct) - if err != nil { - return nil, fmt.Errorf("failed to create proxy dialer: %w", err) - } - - // Proxy HTTP requests (trackers, webseeds) - cfg.HTTPProxy = func(*http.Request) (*url.URL, error) { - return parsed, nil - } - - // Proxy peer connections via DialContext - if contextDialer, ok := dialer.(proxy.ContextDialer); ok { - cfg.HTTPDialContext = contextDialer.DialContext - } - - logger.Info("torrent proxy configured", "proxy", proxyURL) - } - - client, err := torrent.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("failed to create torrent client: %w", err) - } - - hls, err := NewHLSTranscoder(logger) - if err != nil { - logger.Warn("HLS transcoding unavailable", "error", err) - // Continue without HLS - will fall back to direct streaming - } - - return &Service{ - client: client, - nyaa: nyaa.NewClient(), - hls: hls, - activeTorrents: make(map[string]*torrent.Torrent), - logger: logger, - }, nil -} - -// SearchEpisode searches nyaa for torrents of a specific episode -func (s *Service) SearchEpisode(animeTitle string, episode int) ([]nyaa.Torrent, error) { - return s.nyaa.SearchEpisode(animeTitle, episode) -} - -// SearchAnime searches nyaa for torrents of an anime -func (s *Service) SearchAnime(query string) ([]nyaa.Torrent, error) { - return s.nyaa.SearchAnime(query) -} - -// AddMagnet adds a magnet link and returns stream info -func (s *Service) AddMagnet(ctx context.Context, magnetURI string) (*StreamInfo, error) { - t, err := s.client.AddMagnet(magnetURI) - if err != nil { - return nil, fmt.Errorf("failed to add magnet: %w", err) - } - - // Wait for metadata with timeout - select { - case <-t.GotInfo(): - case <-ctx.Done(): - t.Drop() - return nil, ctx.Err() - case <-time.After(60 * time.Second): - t.Drop() - return nil, fmt.Errorf("timeout waiting for torrent metadata") - } - - infoHash := t.InfoHash().HexString() - - s.mu.Lock() - s.activeTorrents[infoHash] = t - s.mu.Unlock() - - return s.getStreamInfo(t), nil -} - -// GetTorrent returns an active torrent by info hash -func (s *Service) GetTorrent(infoHash string) (*torrent.Torrent, bool) { - s.mu.RLock() - defer s.mu.RUnlock() - t, ok := s.activeTorrents[infoHash] - return t, ok -} - -// StreamFile streams a specific file from a torrent -func (s *Service) StreamFile(w http.ResponseWriter, r *http.Request, infoHash string, fileIdx int) error { - t, ok := s.GetTorrent(infoHash) - if !ok { - return fmt.Errorf("torrent not found: %s", infoHash) - } - - files := t.Files() - if fileIdx < 0 || fileIdx >= len(files) { - return fmt.Errorf("invalid file index: %d", fileIdx) - } - - file := files[fileIdx] - reader := file.NewReader() - reader.SetReadahead(file.Length() / 100) // 1% readahead - reader.SetResponsive() - - // Determine content type - contentType := "application/octet-stream" - ext := strings.ToLower(filepath.Ext(file.Path())) - switch ext { - case ".mp4": - contentType = "video/mp4" - case ".mkv": - contentType = "video/x-matroska" - case ".webm": - contentType = "video/webm" - case ".avi": - contentType = "video/x-msvideo" - } - - w.Header().Set("Content-Type", contentType) - w.Header().Set("Accept-Ranges", "bytes") - - // Handle range requests for seeking - http.ServeContent(w, r, file.Path(), time.Time{}, reader) - return nil -} - -// StreamVideo finds and streams the main video file from a torrent -func (s *Service) StreamVideo(w http.ResponseWriter, r *http.Request, infoHash string) error { - t, ok := s.GetTorrent(infoHash) - if !ok { - return fmt.Errorf("torrent not found: %s", infoHash) - } - - // Find the largest video file - var bestFile *torrent.File - var bestIdx int - for i, f := range t.Files() { - if isVideoFile(f.Path()) { - if bestFile == nil || f.Length() > bestFile.Length() { - bestFile = f - bestIdx = i - } - } - } - - if bestFile == nil { - return fmt.Errorf("no video file found in torrent") - } - - return s.StreamFile(w, r, infoHash, bestIdx) -} - -// GetStreamInfo returns info about an active torrent -func (s *Service) GetStreamInfo(infoHash string) (*StreamInfo, error) { - t, ok := s.GetTorrent(infoHash) - if !ok { - return nil, fmt.Errorf("torrent not found: %s", infoHash) - } - return s.getStreamInfo(t), nil -} - -func (s *Service) getStreamInfo(t *torrent.Torrent) *StreamInfo { - info := &StreamInfo{ - InfoHash: t.InfoHash().HexString(), - Name: t.Name(), - Peers: t.Stats().ActivePeers, - } - - var totalLength, completed int64 - for i, f := range t.Files() { - info.Files = append(info.Files, FileInfo{ - Index: i, - Path: f.Path(), - Size: f.Length(), - }) - totalLength += f.Length() - completed += f.BytesCompleted() - } - - info.Size = totalLength - if totalLength > 0 { - info.Progress = float64(completed) / float64(totalLength) * 100 - } - - stats := t.Stats() - info.DownloadRate = stats.ConnStats.BytesReadData.Int64() - - return info -} - -// DropTorrent removes a torrent from the client -func (s *Service) DropTorrent(infoHash string) { - s.mu.Lock() - defer s.mu.Unlock() - - if t, ok := s.activeTorrents[infoHash]; ok { - t.Drop() - delete(s.activeTorrents, infoHash) - } -} - -// Close shuts down the torrent client -func (s *Service) Close() { - s.mu.Lock() - for _, t := range s.activeTorrents { - t.Drop() - } - s.activeTorrents = nil - s.mu.Unlock() - - if s.hls != nil { - s.hls.Shutdown() - } - - s.client.Close() -} - -func isVideoFile(path string) bool { - videoExts := []string{".mp4", ".mkv", ".avi", ".mov", ".webm", ".wmv", ".flv"} - lower := strings.ToLower(path) - for _, ext := range videoExts { - if strings.HasSuffix(lower, ext) { - return true - } - } - return false -} - -// findEpisodeFile finds the video file matching a specific episode number -// Falls back to largest video file if no match found -func findEpisodeFile(files []*torrent.File, episode int) *torrent.File { - var bestMatch *torrent.File - var fallback *torrent.File - - // Episode patterns to match in filenames - epStr := fmt.Sprintf("%02d", episode) - epStr2 := fmt.Sprintf("%d", episode) - - patterns := []string{ - fmt.Sprintf(" - %s", epStr), // - 01 - fmt.Sprintf(" - %s ", epStr), // - 01 (with space after) - fmt.Sprintf("E%s", epStr), // E01 - fmt.Sprintf("E%s ", epStr), // E01 (with space) - fmt.Sprintf("Episode %s", epStr2), // Episode 1 - fmt.Sprintf("Episode %s", epStr), // Episode 01 - fmt.Sprintf(" %s ", epStr), // standalone 01 - fmt.Sprintf("[%s]", epStr), // [01] - fmt.Sprintf("_%s_", epStr), // _01_ - fmt.Sprintf(".%s.", epStr), // .01. - } - - for _, f := range files { - if !isVideoFile(f.Path()) { - continue - } - - // Track largest video as fallback - if fallback == nil || f.Length() > fallback.Length() { - fallback = f - } - - filename := strings.ToLower(filepath.Base(f.Path())) - - // Check each pattern - for _, pattern := range patterns { - if strings.Contains(filename, strings.ToLower(pattern)) { - // Verify it's not a different episode (e.g., searching for ep 1, don't match ep 10) - // Check character after match isn't a digit - idx := strings.Index(filename, strings.ToLower(pattern)) - if idx >= 0 { - afterIdx := idx + len(pattern) - if afterIdx >= len(filename) || !isDigit(filename[afterIdx]) { - if bestMatch == nil || f.Length() > bestMatch.Length() { - bestMatch = f - } - break - } - } - } - } - } - - if bestMatch != nil { - return bestMatch - } - return fallback -} - -func isDigit(c byte) bool { - return c >= '0' && c <= '9' -} - -// ParseMagnetHash extracts info hash from a magnet URI -func ParseMagnetHash(magnetURI string) (string, error) { - spec, err := torrent.TorrentSpecFromMagnetUri(magnetURI) - if err != nil { - return "", err - } - return spec.InfoHash.HexString(), nil -} - -// MagnetFromHash creates a minimal magnet URI from an info hash -func MagnetFromHash(infoHash string) (string, error) { - var ih metainfo.Hash - if err := ih.FromHexString(infoHash); err != nil { - return "", err - } - return fmt.Sprintf("magnet:?xt=urn:btih:%s", ih.HexString()), nil -} - -// GetVideoFilePath returns the filesystem path to the main video file -func (s *Service) GetVideoFilePath(infoHash string) (string, error) { - t, ok := s.GetTorrent(infoHash) - if !ok { - return "", fmt.Errorf("torrent not found: %s", infoHash) - } - - // Find the largest video file - var bestFile *torrent.File - for _, f := range t.Files() { - if isVideoFile(f.Path()) { - if bestFile == nil || f.Length() > bestFile.Length() { - bestFile = f - } - } - } - - if bestFile == nil { - return "", fmt.Errorf("no video file found in torrent") - } - - // Return path relative to data dir - return filepath.Join("/tmp", "mal-streams", bestFile.Path()), nil -} - -// StartHLS starts HLS transcoding for a torrent -// If episode > 0, it will try to find the file matching that episode number -func (s *Service) StartHLS(ctx context.Context, infoHash string, episode int) (*HLSSession, error) { - if s.hls == nil { - return nil, fmt.Errorf("HLS transcoding not available (ffmpeg not found)") - } - - // Use episode-specific session key if episode specified - sessionKey := infoHash - if episode > 0 { - sessionKey = fmt.Sprintf("%s-ep%d", infoHash, episode) - } - - // Check if session already exists - if session, ok := s.hls.GetSession(sessionKey); ok { - return session, nil - } - - // Get torrent and video file - t, ok := s.GetTorrent(infoHash) - if !ok { - return nil, fmt.Errorf("torrent not found: %s", infoHash) - } - - // Find the video file - either by episode or largest - var videoFile *torrent.File - if episode > 0 { - videoFile = findEpisodeFile(t.Files(), episode) - if videoFile != nil { - s.logger.Info("found episode file", "episode", episode, "file", videoFile.Path()) - } - } - - // Fallback to largest video file - if videoFile == nil { - for _, f := range t.Files() { - if isVideoFile(f.Path()) { - if videoFile == nil || f.Length() > videoFile.Length() { - videoFile = f - } - } - } - } - - if videoFile == nil { - return nil, fmt.Errorf("no video file found in torrent") - } - - // Prioritize downloading the beginning of the file for ffmpeg - videoFile.Download() - reader := videoFile.NewReader() - reader.SetReadahead(10 * 1024 * 1024) // 10MB readahead - reader.SetResponsive() - - // Wait for at least 2MB to be available before starting ffmpeg - minBytes := int64(2 * 1024 * 1024) - s.logger.Info("waiting for initial data", "hash", infoHash, "file", videoFile.Path(), "need", minBytes) - - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - - timeout := time.After(60 * time.Second) - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-timeout: - return nil, fmt.Errorf("timeout waiting for video data") - case <-ticker.C: - completed := videoFile.BytesCompleted() - if completed >= minBytes { - s.logger.Info("got enough data, starting transcoding", "hash", infoHash, "bytes", completed) - goto ready - } - s.logger.Debug("waiting for data", "hash", infoHash, "completed", completed, "need", minBytes) - } - } - -ready: - // Start transcoding with the reader piped to ffmpeg - session, err := s.hls.StartSessionWithReader(sessionKey, reader) - if err != nil { - return nil, err - } - - // Wait for first segments to be ready - waitCtx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - - if err := session.WaitReady(waitCtx); err != nil { - s.hls.StopSession(sessionKey) - return nil, fmt.Errorf("HLS not ready: %w", err) - } - - return session, nil -} - -// GetHLSSession returns an existing HLS session -func (s *Service) GetHLSSession(infoHash string) (*HLSSession, bool) { - if s.hls == nil { - return nil, false - } - return s.hls.GetSession(infoHash) -} - -// HasHLS returns whether HLS transcoding is available -func (s *Service) HasHLS() bool { - return s.hls != nil -} diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ index 7443383..36c6057 100644 --- a/internal/templates/anime.templ +++ b/internal/templates/anime.templ @@ -56,14 +56,6 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) { loading relations -
-
-
-
-
- loading episodes -
-