diff --git a/go.mod b/go.mod
index 8517af4..17c1a9f 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,6 @@ require (
 	github.com/bodgit/sevenzip v1.5.1
 	github.com/dgraph-io/badger/v4 v4.2.0
 	github.com/dustin/go-humanize v1.0.1
-	github.com/gin-gonic/gin v1.9.1
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/gofrs/uuid/v5 v5.1.0
 	github.com/google/go-github/v63 v63.0.0
@@ -34,6 +33,7 @@ require (
 	github.com/prometheus/client_golang v1.19.1
 	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93
 	github.com/ravilushqa/otelgqlgen v0.15.0
+	github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450
 	github.com/royalcat/ctxio v0.0.0-20240602060200-590d464c39be
 	github.com/royalcat/ctxprogress v0.0.0-20240614113930-3cc5bb935bff
 	github.com/royalcat/kv v0.0.0-20240707205211-fedd4883af85
@@ -86,13 +86,9 @@ require (
 	github.com/bodgit/plumbing v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
-	github.com/bytedance/sonic v1.11.9 // indirect
-	github.com/bytedance/sonic/loader v0.2.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash v1.1.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/cloudwego/base64x v0.1.4 // indirect
-	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.5 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -100,17 +96,11 @@ require (
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
-	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-llsqlite/adapter v0.1.0 // indirect
 	github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
-	github.com/go-playground/locales v0.14.1 // indirect
-	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.20.0 // indirect
 	github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
-	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/golang/glog v1.2.1 // indirect
@@ -126,27 +116,22 @@ require (
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/huandu/xstrings v1.4.0 // indirect
-	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.8 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/labstack/gommon v0.4.2 // indirect
-	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/minio/sha256-simd v1.0.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mr-tron/base58 v1.2.0 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
 	github.com/multiformats/go-multihash v0.2.3 // indirect
 	github.com/multiformats/go-varint v0.0.6 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
 	github.com/pion/datachannel v1.5.6 // indirect
 	github.com/pion/dtls/v2 v2.2.11 // indirect
@@ -179,8 +164,6 @@ require (
 	github.com/sosodev/duration v1.3.1 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/tidwall/btree v1.7.0 // indirect
-	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/ulikunitz/xz v0.5.12 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
@@ -192,7 +175,6 @@ require (
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
 	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
-	golang.org/x/arch v0.8.0 // indirect
 	golang.org/x/crypto v0.24.0 // indirect
 	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/text v0.16.0 // indirect
diff --git a/go.sum b/go.sum
index b7203a6..d1076ad 100644
--- a/go.sum
+++ b/go.sum
@@ -136,11 +136,6 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w
 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/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
-github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
-github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
-github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
-github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -153,10 +148,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
-github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
-github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -200,12 +191,6 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
-github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
 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=
@@ -231,20 +216,10 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
-github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
-github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
-github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
-github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
 github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
@@ -356,8 +331,6 @@ github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -384,7 +357,6 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd
 github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
 github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
 github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
-github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 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=
@@ -402,8 +374,6 @@ github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+k
 github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
-github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
-github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
 github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -422,12 +392,9 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 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=
@@ -451,8 +418,6 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 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/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
@@ -557,6 +522,8 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450 h1:AZyZxXZLniAR0DaZhTS4RVcHtOvYMW8IunplqC9A0mk=
+github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450/go.mod h1:m3TPa9l/wMKpm/7WHrMs3dSFUxo7kLHaI8ap+SFGYhQ=
 github.com/royalcat/ctxio v0.0.0-20240602060200-590d464c39be h1:Ui+Imq1Vk26rfpkLUsgvVdYO/UOJkzDyPzESfrTqWfM=
 github.com/royalcat/ctxio v0.0.0-20240602060200-590d464c39be/go.mod h1:NFNp3OsEMUPYj5LZUFDiyDt+2E6gR/g8JLd0k+y8XWI=
 github.com/royalcat/ctxprogress v0.0.0-20240614113930-3cc5bb935bff h1:KlZaOEZYhCzyNYIp0LcE7MNR2Ar0PJS3eJU6A5mMTpk=
@@ -625,10 +592,6 @@ github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EU
 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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
-github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
-github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
 github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -697,9 +660,6 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
 go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
-golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
-golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -1016,9 +976,7 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
 modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
diff --git a/pkg/qbittorrent/application.go b/pkg/qbittorrent/application.go
index 31daefd..3fa5587 100644
--- a/pkg/qbittorrent/application.go
+++ b/pkg/qbittorrent/application.go
@@ -240,6 +240,9 @@ type Preferences struct {
 }
 
 func (c *client) Version(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.Version")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/version", c.config.Address)
 
 	result, err := c.doRequest(ctx, &requestData{
@@ -257,6 +260,9 @@ func (c *client) Version(ctx context.Context) (string, error) {
 }
 
 func (c *client) WebApiVersion(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.WebApiVersion")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/webapiVersion", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -273,6 +279,9 @@ func (c *client) WebApiVersion(ctx context.Context) (string, error) {
 }
 
 func (c *client) BuildInfo(ctx context.Context) (*BuildInfo, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.BuildInfo")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/buildInfo", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -294,6 +303,9 @@ func (c *client) BuildInfo(ctx context.Context) (*BuildInfo, error) {
 }
 
 func (c *client) Shutdown(ctx context.Context) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.Shutdown")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/shutdown", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		method: http.MethodPost,
@@ -311,6 +323,9 @@ func (c *client) Shutdown(ctx context.Context) error {
 }
 
 func (c *client) GetPreferences(ctx context.Context) (*Preferences, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.GetPreferences")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/preferences", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -332,6 +347,9 @@ func (c *client) GetPreferences(ctx context.Context) (*Preferences, error) {
 }
 
 func (c *client) SetPreferences(ctx context.Context, prefs *Preferences) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.SetPreferences")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/setPreferences", c.config.Address)
 	data, err := json.Marshal(prefs)
 	if err != nil {
@@ -359,6 +377,9 @@ func (c *client) SetPreferences(ctx context.Context, prefs *Preferences) error {
 }
 
 func (c *client) DefaultSavePath(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Application.DefaultSavePath")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/app/defaultSavePath", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
diff --git a/pkg/qbittorrent/authentication.go b/pkg/qbittorrent/authentication.go
index eb91b2c..f975d7f 100644
--- a/pkg/qbittorrent/authentication.go
+++ b/pkg/qbittorrent/authentication.go
@@ -19,6 +19,9 @@ type Authentication interface {
 }
 
 func (c *client) Login(ctx context.Context) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Login")
+	defer span.End()
+
 	if c.config.Username == "" || c.config.Password == "" {
 		return errors.New("username or password is empty")
 	}
@@ -51,8 +54,8 @@ func (c *client) Login(ctx context.Context) error {
 		return errors.New("login failed: " + string(result.body))
 	}
 
-	if c.cookieJar == nil {
-		c.cookieJar, err = cookiejar.New(nil)
+	if c.client.Jar == nil {
+		c.client.Jar, err = cookiejar.New(nil)
 		if err != nil {
 			return err
 		}
@@ -62,12 +65,15 @@ func (c *client) Login(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	c.cookieJar.SetCookies(u, result.cookies)
+	c.client.Jar.SetCookies(u, result.cookies)
 
 	return nil
 }
 
 func (c *client) Logout(ctx context.Context) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Logout")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/auth/logout", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		method: http.MethodPost,
diff --git a/pkg/qbittorrent/client.go b/pkg/qbittorrent/client.go
index 2ec6fba..fb259d6 100644
--- a/pkg/qbittorrent/client.go
+++ b/pkg/qbittorrent/client.go
@@ -1,6 +1,16 @@
 package qbittorrent
 
-import "context"
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"net/http"
+	"time"
+
+	"go.opentelemetry.io/otel"
+)
+
+var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/pkg/qbittorrent")
 
 // Client represents a qBittorrent client
 type Client interface {
@@ -23,12 +33,12 @@ type Client interface {
 }
 
 func NewClient(ctx context.Context, cfg *Config) (Client, error) {
-	var c = &client{config: cfg, clientPool: newClientPool(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
+	var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
 	return c, nil
 }
 
 func LoginClient(ctx context.Context, cfg *Config) (Client, error) {
-	var c = &client{config: cfg, clientPool: newClientPool(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
+	var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
 	if err := c.Authentication().Login(ctx); err != nil {
 		return nil, err
 	}
@@ -37,3 +47,26 @@ func LoginClient(ctx context.Context, cfg *Config) (Client, error) {
 	}
 	return c, nil
 }
+
+// newClient creates and returns a new clientPool
+func newClient(maxIdle int, timeout time.Duration) *http.Client {
+	if maxIdle == 0 {
+		maxIdle = 128
+	}
+	if timeout == 0 {
+		timeout = time.Second * 3
+	}
+
+	return &http.Client{
+		Transport: &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			DialContext: (&net.Dialer{
+				Timeout:   30 * time.Second,
+				KeepAlive: 30 * time.Second,
+			}).DialContext,
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			MaxIdleConns:    maxIdle,
+		},
+		Timeout: timeout,
+	}
+}
diff --git a/pkg/qbittorrent/client_impl.go b/pkg/qbittorrent/client_impl.go
index 5151fd5..21d167d 100644
--- a/pkg/qbittorrent/client_impl.go
+++ b/pkg/qbittorrent/client_impl.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"net/http/cookiejar"
 	"net/url"
 	"strings"
 	"time"
@@ -27,9 +26,8 @@ type requestData struct {
 var _ Client = (*client)(nil)
 
 type client struct {
-	config     *Config
-	clientPool *clientPool
-	cookieJar  *cookiejar.Jar
+	config *Config
+	client *http.Client
 }
 
 func (c *client) Authentication() Authentication {
@@ -81,13 +79,8 @@ func (c *client) doRequest(ctx context.Context, data *requestData) (*responseRes
 	for key, value := range c.config.CustomHeaders {
 		request.Header.Set(key, value)
 	}
-	hc := c.clientPool.GetClient()
-	defer c.clientPool.ReleaseClient(hc)
-	if c.cookieJar != nil {
-		hc.Jar = c.cookieJar
-	}
 
-	resp, err := hc.Do(request)
+	resp, err := c.client.Do(request)
 	if err != nil {
 		return nil, err
 	}
@@ -102,14 +95,14 @@ func (c *client) doRequest(ctx context.Context, data *requestData) (*responseRes
 }
 
 func (c *client) cookies() (string, error) {
-	if c.cookieJar == nil {
+	if c.client.Jar == nil {
 		return "", ErrNotLogin
 	}
 	u, err := url.Parse(c.config.Address)
 	if err != nil {
 		return "", err
 	}
-	cookies := c.cookieJar.Cookies(u)
+	cookies := c.client.Jar.Cookies(u)
 	if len(cookies) == 0 {
 		return "", ErrNotLogin
 	}
diff --git a/pkg/qbittorrent/client_pool.go b/pkg/qbittorrent/client_pool.go
deleted file mode 100644
index d390665..0000000
--- a/pkg/qbittorrent/client_pool.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package qbittorrent
-
-import (
-	"crypto/tls"
-	"net"
-	"net/http"
-	"sync"
-	"time"
-)
-
-// clientPool defines a pool of HTTP clients
-type clientPool struct {
-	// pool store http.Client instances
-	*sync.Pool
-}
-
-// newClientPool creates and returns a new clientPool
-func newClientPool(maxIdle int, timeout time.Duration) *clientPool {
-	if maxIdle == 0 {
-		maxIdle = 128
-	}
-	if timeout == 0 {
-		timeout = time.Second * 3
-	}
-	return &clientPool{
-		Pool: &sync.Pool{
-			New: func() any {
-				return &http.Client{
-					Transport: &http.Transport{
-						Proxy: http.ProxyFromEnvironment,
-						DialContext: (&net.Dialer{
-							Timeout:   30 * time.Second,
-							KeepAlive: 30 * time.Second,
-						}).DialContext,
-						TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-						MaxIdleConns:    maxIdle,
-					},
-					Timeout: timeout,
-				}
-			},
-		},
-	}
-}
-
-// GetClient retrieves a http.Client from the pool
-func (p *clientPool) GetClient() *http.Client {
-	return p.Get().(*http.Client)
-}
-
-// ReleaseClient returns a http.Client back to the pool
-func (p *clientPool) ReleaseClient(client *http.Client) {
-	p.Put(client)
-}
diff --git a/pkg/qbittorrent/log.go b/pkg/qbittorrent/log.go
index d81fbef..bfb7110 100644
--- a/pkg/qbittorrent/log.go
+++ b/pkg/qbittorrent/log.go
@@ -36,6 +36,9 @@ type Log interface {
 }
 
 func (c *client) GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Log.GetLog")
+	defer span.End()
+
 	var form = url.Values{}
 	err := encoder.Encode(option, form)
 	if err != nil {
@@ -64,6 +67,9 @@ func (c *client) GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, er
 }
 
 func (c *client) GetPeerLog(ctx context.Context, lastKnownId int) ([]*LogEntry, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Log.GetPeerLog")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/log/peers", c.config.Address)
 	var form = url.Values{}
 	form.Add("last_known_id", strconv.Itoa(lastKnownId))
diff --git a/pkg/qbittorrent/rss.go b/pkg/qbittorrent/rss.go
index 2e5da65..09f141a 100644
--- a/pkg/qbittorrent/rss.go
+++ b/pkg/qbittorrent/rss.go
@@ -87,6 +87,9 @@ type RssAutoDownloadingRuleDef struct {
 }
 
 func (c *client) AddFolder(ctx context.Context, path string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFolder")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("path", path)
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/addFolder", c.config.Address)
@@ -106,6 +109,9 @@ func (c *client) AddFolder(ctx context.Context, path string) error {
 }
 
 func (c *client) AddFeed(ctx context.Context, opt *RssAddFeedOption) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFeed")
+	defer span.End()
+
 	var formData = url.Values{}
 	err := encoder.Encode(opt, formData)
 	if err != nil {
@@ -128,6 +134,9 @@ func (c *client) AddFeed(ctx context.Context, opt *RssAddFeedOption) error {
 }
 
 func (c *client) RemoveItem(ctx context.Context, path string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveItem")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("path", path)
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeItem", c.config.Address)
@@ -147,6 +156,9 @@ func (c *client) RemoveItem(ctx context.Context, path string) error {
 }
 
 func (c *client) MoveItem(ctx context.Context, srcPath, destPath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.MoveItem")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("itemPath", srcPath)
 	formData.Add("destPath", destPath)
@@ -167,6 +179,9 @@ func (c *client) MoveItem(ctx context.Context, srcPath, destPath string) error {
 }
 
 func (c *client) GetItems(ctx context.Context, withData bool) (map[string]interface{}, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetItems")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/items?withData=%t", c.config.Address, withData)
 	result, err := c.doRequest(ctx, &requestData{
 		url:    apiUrl,
@@ -185,6 +200,9 @@ func (c *client) GetItems(ctx context.Context, withData bool) (map[string]interf
 }
 
 func (c *client) MarkAsRead(ctx context.Context, opt *RssMarkAsReadOption) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.MarkAsRead")
+	defer span.End()
+
 	var formData = url.Values{}
 	err := encoder.Encode(opt, formData)
 	if err != nil {
@@ -207,6 +225,9 @@ func (c *client) MarkAsRead(ctx context.Context, opt *RssMarkAsReadOption) error
 }
 
 func (c *client) RefreshItem(ctx context.Context, itemPath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.RefreshItem")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("itemPath", itemPath)
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/refreshItem", c.config.Address)
@@ -226,6 +247,9 @@ func (c *client) RefreshItem(ctx context.Context, itemPath string) error {
 }
 
 func (c *client) SetAutoDownloadingRule(ctx context.Context, ruleName string, ruleDef *RssAutoDownloadingRuleDef) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.SetAutoDownloadingRule")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("ruleName", ruleName)
 	ruleDefBytes, err := json.Marshal(ruleDef)
@@ -250,6 +274,9 @@ func (c *client) SetAutoDownloadingRule(ctx context.Context, ruleName string, ru
 }
 
 func (c *client) RenameAutoDownloadingRule(ctx context.Context, ruleName, newRuleName string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.RenameAutoDownloadingRule")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("ruleName", ruleName)
 	formData.Add("newRuleName", newRuleName)
@@ -270,6 +297,9 @@ func (c *client) RenameAutoDownloadingRule(ctx context.Context, ruleName, newRul
 }
 
 func (c *client) RemoveAutoDownloadingRule(ctx context.Context, ruleName string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveAutoDownloadingRule")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("ruleName", ruleName)
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeRule", c.config.Address)
@@ -289,6 +319,9 @@ func (c *client) RemoveAutoDownloadingRule(ctx context.Context, ruleName string)
 }
 
 func (c *client) GetAllAutoDownloadingRules(ctx context.Context) (map[string]*RssAutoDownloadingRuleDef, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllAutoDownloadingRules")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -305,6 +338,9 @@ func (c *client) GetAllAutoDownloadingRules(ctx context.Context) (map[string]*Rs
 }
 
 func (c *client) GetAllArticlesMatchingRule(ctx context.Context, ruleName string) (map[string][]string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllArticlesMatchingRule")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("ruleName", ruleName)
 	var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles?%s", c.config.Address, formData.Encode())
diff --git a/pkg/qbittorrent/sync.go b/pkg/qbittorrent/sync.go
index cfd5abc..4518a01 100644
--- a/pkg/qbittorrent/sync.go
+++ b/pkg/qbittorrent/sync.go
@@ -76,6 +76,9 @@ type SyncTorrentPeer struct {
 }
 
 func (c *client) MainData(ctx context.Context, rid int) (*SyncMainData, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Sync.MainData")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/sync/maindata?rid=%d", c.config.Address, rid)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -97,6 +100,9 @@ func (c *client) MainData(ctx context.Context, rid int) (*SyncMainData, error) {
 }
 
 func (c *client) TorrentPeersData(ctx context.Context, hash string, rid int) (*SyncTorrentPeers, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Sync.TorrentPeersData")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hash", hash)
 	formData.Add("rid", strconv.Itoa(rid))
diff --git a/pkg/qbittorrent/torrent.go b/pkg/qbittorrent/torrent.go
index 298955e..f7c62c2 100644
--- a/pkg/qbittorrent/torrent.go
+++ b/pkg/qbittorrent/torrent.go
@@ -62,7 +62,7 @@ type Torrent interface {
 	// MinPriority minimal torrent priority
 	MinPriority(ctx context.Context, hashes []string) error
 	// SetFilePriority set file priority
-	SetFilePriority(ctx context.Context, hash string, id string, priority int) error
+	SetFilePriority(ctx context.Context, hash string, id int, priority Priority) error
 	// GetDownloadLimit get torrent download limit
 	GetDownloadLimit(ctx context.Context, hashes []string) (map[string]int, error)
 	// SetDownloadLimit set torrent download limit, limit in bytes per second, if no limit please set value zero
@@ -138,58 +138,82 @@ type TorrentOption struct {
 	Hashes []string `schema:"-"`
 }
 
+type TorrentState string
+
+const (
+	TorrentStateError              TorrentState = "error"
+	TorrentStateMissingFiles       TorrentState = "missingFiles"
+	TorrentStateUploading          TorrentState = "uploading"
+	TorrentStatePausedUP           TorrentState = "pausedUP"
+	TorrentStateQueuedUP           TorrentState = "queuedUP"
+	TorrentStateStalledUP          TorrentState = "stalledUP"
+	TorrentStateCheckingUP         TorrentState = "checkingUP"
+	TorrentStateForcedUP           TorrentState = "forcedUP"
+	TorrentStateAllocating         TorrentState = "allocating"
+	TorrentStateDownloading        TorrentState = "downloading"
+	TorrentStateMetaDL             TorrentState = "metaDL"
+	TorrentStatePausedDL           TorrentState = "pausedDL"
+	TorrentStateQueuedDL           TorrentState = "queuedDL"
+	TorrentStateStalledDL          TorrentState = "stalledDL"
+	TorrentStateCheckingDL         TorrentState = "checkingDL"
+	TorrentStateForcedDL           TorrentState = "forcedDL"
+	TorrentStateCheckingResumeData TorrentState = "checkingResumeData"
+	TorrentStateMoving             TorrentState = "moving"
+	TorrentStateUnknown            TorrentState = "unknown"
+)
+
 type TorrentInfo struct {
-	AddedOn                  int     `json:"added_on"`
-	AmountLeft               int     `json:"amount_left"`
-	AutoTmm                  bool    `json:"auto_tmm"`
-	Availability             float64 `json:"availability"`
-	Category                 string  `json:"category"`
-	Completed                int     `json:"completed"`
-	CompletionOn             int     `json:"completion_on"`
-	ContentPath              string  `json:"content_path"`
-	DlLimit                  int     `json:"dl_limit"`
-	Dlspeed                  int     `json:"dlspeed"`
-	DownloadPath             string  `json:"download_path"`
-	Downloaded               int     `json:"downloaded"`
-	DownloadedSession        int     `json:"downloaded_session"`
-	Eta                      int     `json:"eta"`
-	FLPiecePrio              bool    `json:"f_l_piece_prio"`
-	ForceStart               bool    `json:"force_start"`
-	Hash                     string  `json:"hash"`
-	InactiveSeedingTimeLimit int     `json:"inactive_seeding_time_limit"`
-	InfohashV1               string  `json:"infohash_v1"`
-	InfohashV2               string  `json:"infohash_v2"`
-	LastActivity             int     `json:"last_activity"`
-	MagnetURI                string  `json:"magnet_uri"`
-	MaxInactiveSeedingTime   int     `json:"max_inactive_seeding_time"`
-	MaxRatio                 int     `json:"max_ratio"`
-	MaxSeedingTime           int     `json:"max_seeding_time"`
-	Name                     string  `json:"name"`
-	NumComplete              int     `json:"num_complete"`
-	NumIncomplete            int     `json:"num_incomplete"`
-	NumLeechs                int     `json:"num_leechs"`
-	NumSeeds                 int     `json:"num_seeds"`
-	Priority                 int     `json:"priority"`
-	Progress                 float64 `json:"progress"`
-	Ratio                    float64 `json:"ratio"`
-	RatioLimit               int     `json:"ratio_limit"`
-	SavePath                 string  `json:"save_path"`
-	SeedingTime              int     `json:"seeding_time"`
-	SeedingTimeLimit         int     `json:"seeding_time_limit"`
-	SeenComplete             int     `json:"seen_complete"`
-	SeqDl                    bool    `json:"seq_dl"`
-	Size                     int     `json:"size"`
-	State                    string  `json:"state"`
-	SuperSeeding             bool    `json:"super_seeding"`
-	Tags                     string  `json:"tags"`
-	TimeActive               int     `json:"time_active"`
-	TotalSize                int     `json:"total_size"`
-	Tracker                  string  `json:"tracker"`
-	TrackersCount            int     `json:"trackers_count"`
-	UpLimit                  int     `json:"up_limit"`
-	Uploaded                 int     `json:"uploaded"`
-	UploadedSession          int     `json:"uploaded_session"`
-	Upspeed                  int     `json:"upspeed"`
+	AddedOn                  int          `json:"added_on"`
+	AmountLeft               int          `json:"amount_left"`
+	AutoTmm                  bool         `json:"auto_tmm"`
+	Availability             float64      `json:"availability"`
+	Category                 string       `json:"category"`
+	Completed                int          `json:"completed"`
+	CompletionOn             int          `json:"completion_on"`
+	ContentPath              string       `json:"content_path"`
+	DlLimit                  int          `json:"dl_limit"`
+	Dlspeed                  int          `json:"dlspeed"`
+	DownloadPath             string       `json:"download_path"`
+	Downloaded               int          `json:"downloaded"`
+	DownloadedSession        int          `json:"downloaded_session"`
+	Eta                      int          `json:"eta"`
+	FLPiecePrio              bool         `json:"f_l_piece_prio"`
+	ForceStart               bool         `json:"force_start"`
+	Hash                     string       `json:"hash"`
+	InactiveSeedingTimeLimit int          `json:"inactive_seeding_time_limit"`
+	InfohashV1               string       `json:"infohash_v1"`
+	InfohashV2               string       `json:"infohash_v2"`
+	LastActivity             int          `json:"last_activity"`
+	MagnetURI                string       `json:"magnet_uri"`
+	MaxInactiveSeedingTime   int          `json:"max_inactive_seeding_time"`
+	MaxRatio                 int          `json:"max_ratio"`
+	MaxSeedingTime           int          `json:"max_seeding_time"`
+	Name                     string       `json:"name"`
+	NumComplete              int          `json:"num_complete"`
+	NumIncomplete            int          `json:"num_incomplete"`
+	NumLeechs                int          `json:"num_leechs"`
+	NumSeeds                 int          `json:"num_seeds"`
+	Priority                 int          `json:"priority"`
+	Progress                 float64      `json:"progress"`
+	Ratio                    float64      `json:"ratio"`
+	RatioLimit               int          `json:"ratio_limit"`
+	SavePath                 string       `json:"save_path"`
+	SeedingTime              int          `json:"seeding_time"`
+	SeedingTimeLimit         int          `json:"seeding_time_limit"`
+	SeenComplete             int          `json:"seen_complete"`
+	SeqDl                    bool         `json:"seq_dl"`
+	Size                     int          `json:"size"`
+	State                    TorrentState `json:"state"`
+	SuperSeeding             bool         `json:"super_seeding"`
+	Tags                     string       `json:"tags"`
+	TimeActive               int          `json:"time_active"`
+	TotalSize                int          `json:"total_size"`
+	Tracker                  string       `json:"tracker"`
+	TrackersCount            int          `json:"trackers_count"`
+	UpLimit                  int          `json:"up_limit"`
+	Uploaded                 int          `json:"uploaded"`
+	UploadedSession          int          `json:"uploaded_session"`
+	Upspeed                  int          `json:"upspeed"`
 }
 
 type TorrentProperties struct {
@@ -249,15 +273,24 @@ type TorrentWebSeed struct {
 	URL string `json:"url"`
 }
 
+type Priority int
+
+const (
+	PriorityDoNotDownload Priority = 0
+	PriorityNormal        Priority = 1
+	PriorityHigh          Priority = 6
+	PriorityMax           Priority = 7
+)
+
 type TorrentContent struct {
-	Availability float64 `json:"availability,omitempty"`
-	Index        int     `json:"index,omitempty"`
-	IsSeed       bool    `json:"is_seed,omitempty"`
-	Name         string  `json:"name,omitempty"`
-	PieceRange   []int   `json:"piece_range,omitempty"`
-	Priority     int     `json:"priority,omitempty"`
-	Progress     float64 `json:"progress,omitempty"`
-	Size         int64   `json:"size,omitempty"`
+	Availability float64  `json:"availability,omitempty"`
+	Index        int      `json:"index,omitempty"`
+	IsSeed       bool     `json:"is_seed,omitempty"`
+	Name         string   `json:"name,omitempty"`
+	PieceRange   []int    `json:"piece_range,omitempty"`
+	Priority     Priority `json:"priority,omitempty"`
+	Progress     float64  `json:"progress,omitempty"`
+	Size         int64    `json:"size,omitempty"`
 }
 
 type TorrentAddFileMetadata struct {
@@ -294,6 +327,9 @@ type TorrentCategory struct {
 }
 
 func (c *client) GetTorrents(ctx context.Context, opt *TorrentOption) ([]*TorrentInfo, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetTorrents")
+	defer span.End()
+
 	var formData = url.Values{}
 	err := encoder.Encode(opt, formData)
 	if err != nil {
@@ -324,6 +360,9 @@ func (c *client) GetTorrents(ctx context.Context, opt *TorrentOption) ([]*Torren
 }
 
 func (c *client) GetProperties(ctx context.Context, hash string) (*TorrentProperties, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetProperties")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/torrents/properties?hash=%s", c.config.Address, hash)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -345,6 +384,9 @@ func (c *client) GetProperties(ctx context.Context, hash string) (*TorrentProper
 }
 
 func (c *client) GetTrackers(ctx context.Context, hash string) ([]*TorrentTracker, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetTrackers")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/torrents/trackers?hash=%s", c.config.Address, hash)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -366,6 +408,9 @@ func (c *client) GetTrackers(ctx context.Context, hash string) ([]*TorrentTracke
 }
 
 func (c *client) GetWebSeeds(ctx context.Context, hash string) ([]*TorrentWebSeed, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetWebSeeds")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/torrents/webseeds?hash=%s", c.config.Address, hash)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -395,6 +440,9 @@ func sliceItoa[E constraints.Integer](in []E) []string {
 }
 
 func (c *client) GetContents(ctx context.Context, hash string, indexes ...int) ([]*TorrentContent, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetContents")
+	defer span.End()
+
 	var apiUrl string
 	if len(indexes) != 0 {
 
@@ -422,6 +470,9 @@ func (c *client) GetContents(ctx context.Context, hash string, indexes ...int) (
 }
 
 func (c *client) GetPiecesStates(ctx context.Context, hash string) ([]int, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetPiecesStates")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/pieceStates?hash=%s", c.config.Address, hash)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -443,6 +494,9 @@ func (c *client) GetPiecesStates(ctx context.Context, hash string) ([]int, error
 }
 
 func (c *client) GetPiecesHashes(ctx context.Context, hash string) ([]string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetPiecesHashes")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/pieceHashes?hash=%s", c.config.Address, hash)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -464,6 +518,9 @@ func (c *client) GetPiecesHashes(ctx context.Context, hash string) ([]string, er
 }
 
 func (c *client) PauseTorrents(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.PauseTorrents")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no torrent hashes provided")
 	}
@@ -486,6 +543,9 @@ func (c *client) PauseTorrents(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) ResumeTorrents(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.ResumeTorrents")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no torrent hashes provided")
 	}
@@ -508,6 +568,9 @@ func (c *client) ResumeTorrents(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) DeleteTorrents(ctx context.Context, hashes []string, deleteFile bool) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.DeleteTorrents")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no torrent hashes provided")
 	}
@@ -531,6 +594,9 @@ func (c *client) DeleteTorrents(ctx context.Context, hashes []string, deleteFile
 }
 
 func (c *client) RecheckTorrents(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RecheckTorrents")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no torrent hashes provided")
 	}
@@ -553,6 +619,9 @@ func (c *client) RecheckTorrents(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) ReAnnounceTorrents(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.ReAnnounceTorrents")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no torrent hashes provided")
 	}
@@ -575,6 +644,9 @@ func (c *client) ReAnnounceTorrents(ctx context.Context, hashes []string) error
 }
 
 func (c *client) AddNewTorrent(ctx context.Context, opt *TorrentAddOption) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.AddNewTorrent")
+	defer span.End()
+
 	var requestBody bytes.Buffer
 	var writer = multipart.NewWriter(&requestBody)
 
@@ -662,6 +734,9 @@ func (c *client) AddNewTorrent(ctx context.Context, opt *TorrentAddOption) error
 }
 
 func (c *client) AddTrackers(ctx context.Context, hash string, urls []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.AddTrackers")
+	defer span.End()
+
 	if len(urls) == 0 {
 		return errors.New("no torrent tracker provided")
 	}
@@ -685,6 +760,9 @@ func (c *client) AddTrackers(ctx context.Context, hash string, urls []string) er
 }
 
 func (c *client) EditTrackers(ctx context.Context, hash, origUrl, newUrl string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.EditTrackers")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("origUrl", origUrl)
 	formData.Add("newUrl", newUrl)
@@ -706,6 +784,9 @@ func (c *client) EditTrackers(ctx context.Context, hash, origUrl, newUrl string)
 }
 
 func (c *client) RemoveTrackers(ctx context.Context, hash string, urls []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RemoveTrackers")
+	defer span.End()
+
 	if len(urls) == 0 {
 		return errors.New("no torrent tracker provided")
 	}
@@ -729,6 +810,9 @@ func (c *client) RemoveTrackers(ctx context.Context, hash string, urls []string)
 }
 
 func (c *client) AddPeers(ctx context.Context, hashes []string, peers []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.AddPeers")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -755,6 +839,9 @@ func (c *client) AddPeers(ctx context.Context, hashes []string, peers []string)
 }
 
 func (c *client) IncreasePriority(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.IncreasePriority")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -777,6 +864,9 @@ func (c *client) IncreasePriority(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) DecreasePriority(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.DecreasePriority")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -799,6 +889,9 @@ func (c *client) DecreasePriority(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) MaxPriority(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.MaxPriority")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -821,6 +914,9 @@ func (c *client) MaxPriority(ctx context.Context, hashes []string) error {
 }
 
 func (c *client) MinPriority(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.MinPriority")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -842,11 +938,14 @@ func (c *client) MinPriority(ctx context.Context, hashes []string) error {
 	return nil
 }
 
-func (c *client) SetFilePriority(ctx context.Context, hash string, id string, priority int) error {
+func (c *client) SetFilePriority(ctx context.Context, hash string, id int, priority Priority) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetFilePriority")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hash", hash)
-	formData.Add("id", id)
-	formData.Add("priority", strconv.Itoa(priority))
+	formData.Add("id", strconv.Itoa(id))
+	formData.Add("priority", strconv.Itoa(int(priority)))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/filePrio", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url:    apiUrl,
@@ -864,6 +963,9 @@ func (c *client) SetFilePriority(ctx context.Context, hash string, id string, pr
 }
 
 func (c *client) GetDownloadLimit(ctx context.Context, hashes []string) (map[string]int, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetDownloadLimit")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return nil, errors.New("no hashes provided")
 	}
@@ -888,6 +990,9 @@ func (c *client) GetDownloadLimit(ctx context.Context, hashes []string) (map[str
 }
 
 func (c *client) SetDownloadLimit(ctx context.Context, hashes []string, limit int) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetDownloadLimit")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -911,6 +1016,9 @@ func (c *client) SetDownloadLimit(ctx context.Context, hashes []string, limit in
 }
 
 func (c *client) SetShareLimit(ctx context.Context, hashes []string, ratioLimit float64, seedingTimeLimit, inactiveSeedingTimeLimit int) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetShareLimit")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -936,6 +1044,9 @@ func (c *client) SetShareLimit(ctx context.Context, hashes []string, ratioLimit
 }
 
 func (c *client) GetUploadLimit(ctx context.Context, hashes []string) (map[string]int, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetUploadLimit")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return nil, errors.New("no hashes provided")
 	}
@@ -960,6 +1071,9 @@ func (c *client) GetUploadLimit(ctx context.Context, hashes []string) (map[strin
 }
 
 func (c *client) SetUploadLimit(ctx context.Context, hashes []string, limit int) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetUploadLimit")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -983,6 +1097,9 @@ func (c *client) SetUploadLimit(ctx context.Context, hashes []string, limit int)
 }
 
 func (c *client) SetLocation(ctx context.Context, hashes []string, location string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetLocation")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -1006,6 +1123,9 @@ func (c *client) SetLocation(ctx context.Context, hashes []string, location stri
 }
 
 func (c *client) SetName(ctx context.Context, hash string, name string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetName")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hash", hash)
 	formData.Add("name", name)
@@ -1026,6 +1146,9 @@ func (c *client) SetName(ctx context.Context, hash string, name string) error {
 }
 
 func (c *client) SetCategory(ctx context.Context, hashes []string, category string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetCategory")
+	defer span.End()
+
 	if len(hashes) == 0 {
 		return errors.New("no hashes provided")
 	}
@@ -1049,6 +1172,9 @@ func (c *client) SetCategory(ctx context.Context, hashes []string, category stri
 }
 
 func (c *client) GetCategories(ctx context.Context) (map[string]*TorrentCategory, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetCategories")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/categories", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url:    apiUrl,
@@ -1067,6 +1193,9 @@ func (c *client) GetCategories(ctx context.Context) (map[string]*TorrentCategory
 }
 
 func (c *client) AddNewCategory(ctx context.Context, category, savePath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.AddNewCategory")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("category", category)
 	formData.Add("savePath", savePath)
@@ -1087,6 +1216,9 @@ func (c *client) AddNewCategory(ctx context.Context, category, savePath string)
 }
 
 func (c *client) EditCategory(ctx context.Context, category, savePath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.EditCategory")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("category", category)
 	formData.Add("savePath", savePath)
@@ -1107,6 +1239,9 @@ func (c *client) EditCategory(ctx context.Context, category, savePath string) er
 }
 
 func (c *client) RemoveCategories(ctx context.Context, categories []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RemoveCategories")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("categories", strings.Join(categories, "\n"))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/removeCategories", c.config.Address)
@@ -1146,6 +1281,9 @@ func (c *client) AddTags(ctx context.Context, hashes []string, tags []string) er
 }
 
 func (c *client) RemoveTags(ctx context.Context, hashes []string, tags []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RemoveTags")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	formData.Add("tags", strings.Join(tags, ","))
@@ -1166,6 +1304,9 @@ func (c *client) RemoveTags(ctx context.Context, hashes []string, tags []string)
 }
 
 func (c *client) GetTags(ctx context.Context) ([]string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.GetTags")
+	defer span.End()
+
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/tags", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url:    apiUrl,
@@ -1184,6 +1325,9 @@ func (c *client) GetTags(ctx context.Context) ([]string, error) {
 }
 
 func (c *client) CreateTags(ctx context.Context, tags []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.CreateTags")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("tags", strings.Join(tags, ","))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/createTags", c.config.Address)
@@ -1203,6 +1347,9 @@ func (c *client) CreateTags(ctx context.Context, tags []string) error {
 }
 
 func (c *client) DeleteTags(ctx context.Context, tags []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.DeleteTags")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("tags", strings.Join(tags, ","))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/deleteTags", c.config.Address)
@@ -1222,6 +1369,9 @@ func (c *client) DeleteTags(ctx context.Context, tags []string) error {
 }
 
 func (c *client) SetAutomaticManagement(ctx context.Context, hashes []string, enable bool) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetAutomaticManagement")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	formData.Add("enable", strconv.FormatBool(enable))
@@ -1242,6 +1392,9 @@ func (c *client) SetAutomaticManagement(ctx context.Context, hashes []string, en
 }
 
 func (c *client) ToggleSequentialDownload(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.ToggleSequentialDownload")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/toggleSequentialDownload", c.config.Address)
@@ -1261,6 +1414,9 @@ func (c *client) ToggleSequentialDownload(ctx context.Context, hashes []string)
 }
 
 func (c *client) SetFirstLastPiecePriority(ctx context.Context, hashes []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetFirstLastPiecePriority")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	var apiUrl = fmt.Sprintf("%s/api/v2/torrents/toggleFirstLastPiecePrio", c.config.Address)
@@ -1280,6 +1436,9 @@ func (c *client) SetFirstLastPiecePriority(ctx context.Context, hashes []string)
 }
 
 func (c *client) SetForceStart(ctx context.Context, hashes []string, force bool) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetForceStart")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	formData.Add("value", strconv.FormatBool(force))
@@ -1300,6 +1459,9 @@ func (c *client) SetForceStart(ctx context.Context, hashes []string, force bool)
 }
 
 func (c *client) SetSuperSeeding(ctx context.Context, hashes []string, enable bool) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.SetSuperSeeding")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("hashes", strings.Join(hashes, "|"))
 	formData.Add("value", strconv.FormatBool(enable))
@@ -1320,6 +1482,9 @@ func (c *client) SetSuperSeeding(ctx context.Context, hashes []string, enable bo
 }
 
 func (c *client) RenameFile(ctx context.Context, hash, oldPath, newPath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RenameFile")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("oldPath", oldPath)
 	formData.Add("newPath", newPath)
@@ -1341,6 +1506,9 @@ func (c *client) RenameFile(ctx context.Context, hash, oldPath, newPath string)
 }
 
 func (c *client) RenameFolder(ctx context.Context, hash, oldPath, newPath string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Torrent.RenameFolder")
+	defer span.End()
+
 	var formData = url.Values{}
 	formData.Add("oldPath", oldPath)
 	formData.Add("newPath", newPath)
diff --git a/pkg/qbittorrent/transfer.go b/pkg/qbittorrent/transfer.go
index e6f0a51..6619966 100644
--- a/pkg/qbittorrent/transfer.go
+++ b/pkg/qbittorrent/transfer.go
@@ -47,6 +47,9 @@ type Transfer interface {
 }
 
 func (c *client) GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GlobalStatusBar")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/info", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -68,6 +71,9 @@ func (c *client) GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error
 }
 
 func (c *client) BanPeers(ctx context.Context, peers []string) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.BanPeers")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/banPeers", c.config.Address)
 	var form = url.Values{}
 	form.Add("peers", strings.Join(peers, "|"))
@@ -88,6 +94,9 @@ func (c *client) BanPeers(ctx context.Context, peers []string) error {
 }
 
 func (c *client) GetSpeedLimitsMode(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetSpeedLimitsMode")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/speedLimitsMode", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -104,6 +113,9 @@ func (c *client) GetSpeedLimitsMode(ctx context.Context) (string, error) {
 }
 
 func (c *client) ToggleSpeedLimitsMode(ctx context.Context) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.ToggleSpeedLimitsMode")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/toggleSpeedLimitsMode", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url:    apiUrl,
@@ -121,6 +133,9 @@ func (c *client) ToggleSpeedLimitsMode(ctx context.Context) error {
 }
 
 func (c *client) GetGlobalUploadLimit(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalUploadLimit")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/uploadLimit", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -137,6 +152,9 @@ func (c *client) GetGlobalUploadLimit(ctx context.Context) (string, error) {
 }
 
 func (c *client) SetGlobalUploadLimit(ctx context.Context, limit int) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalUploadLimit")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/setUploadLimit?limit=%d", c.config.Address, limit)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -153,6 +171,9 @@ func (c *client) SetGlobalUploadLimit(ctx context.Context, limit int) error {
 }
 
 func (c *client) GetGlobalDownloadLimit(ctx context.Context) (string, error) {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalDownloadLimit")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/downloadLimit", c.config.Address)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
@@ -169,6 +190,9 @@ func (c *client) GetGlobalDownloadLimit(ctx context.Context) (string, error) {
 }
 
 func (c *client) SetGlobalDownloadLimit(ctx context.Context, limit int) error {
+	ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalDownloadLimit")
+	defer span.End()
+
 	apiUrl := fmt.Sprintf("%s/api/v2/transfer/setDownloadLimit?limit=%d", c.config.Address, limit)
 	result, err := c.doRequest(ctx, &requestData{
 		url: apiUrl,
diff --git a/src/delivery/web.go b/src/delivery/web.go
deleted file mode 100644
index 9bcc202..0000000
--- a/src/delivery/web.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package delivery
-
-import (
-	"net/http"
-
-	"github.com/gin-gonic/gin"
-)
-
-var indexHandler = func(c *gin.Context) {
-	c.HTML(http.StatusOK, "index.html", nil)
-}
-
-// var routesHandler = func(ss *torrent.Stats) gin.HandlerFunc {
-// 	return func(c *gin.Context) {
-// 		c.HTML(http.StatusOK, "routes.html", ss.RoutesStats())
-// 	}
-// }
-
-var logsHandler = func(c *gin.Context) {
-	c.HTML(http.StatusOK, "logs.html", nil)
-}
-
-var serversFoldersHandler = func() gin.HandlerFunc {
-	return func(c *gin.Context) {
-		c.HTML(http.StatusOK, "servers.html", nil)
-	}
-}
diff --git a/src/export/nfs/wrapper.go b/src/export/nfs/wrapper.go
index cf19f5d..cebf09e 100644
--- a/src/export/nfs/wrapper.go
+++ b/src/export/nfs/wrapper.go
@@ -86,6 +86,10 @@ func (bfs *fsWrapper) ReadDir(ctx context.Context, path string) ([]fs.FileInfo,
 
 	out := make([]fs.FileInfo, 0, len(ffs))
 	for _, v := range ffs {
+		if v == nil {
+			continue
+		}
+
 		if info, ok := v.(fs.FileInfo); ok {
 			out = append(out, info)
 		} else {
diff --git a/src/sources/qbittorrent/client.go b/src/sources/qbittorrent/client.go
index 96a772d..8263ca9 100644
--- a/src/sources/qbittorrent/client.go
+++ b/src/sources/qbittorrent/client.go
@@ -7,24 +7,75 @@ import (
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
+	"github.com/hashicorp/golang-lru/v2/expirable"
+	"github.com/royalcat/btrgo/btrsync"
 )
 
 type cacheClient struct {
 	qb qbittorrent.Client
+
+	propertiesCache *expirable.LRU[string, qbittorrent.TorrentProperties]
+	torrentsCache   *expirable.LRU[string, qbittorrent.TorrentInfo]
+
+	pieceCache btrsync.MapOf[pieceKey, int]
+}
+
+type pieceKey struct {
+	hash  string
+	index int
 }
 
 func wrapClient(qb qbittorrent.Client) *cacheClient {
-	return &cacheClient{qb: qb}
+
+	const (
+		cacheSize = 5000
+		cacheTTL  = time.Minute
+	)
+
+	return &cacheClient{
+		qb:              qb,
+		propertiesCache: expirable.NewLRU[string, qbittorrent.TorrentProperties](cacheSize, nil, cacheTTL),
+		torrentsCache:   expirable.NewLRU[string, qbittorrent.TorrentInfo](cacheSize, nil, cacheTTL),
+		pieceCache:      btrsync.MapOf[pieceKey, int]{},
+	}
 }
 
-var errNotFound = fmt.Errorf("not found")
+func (f *cacheClient) getInfo(ctx context.Context, hash string) (*qbittorrent.TorrentInfo, error) {
+	if v, ok := f.torrentsCache.Get(hash); ok {
+		return &v, nil
+	}
+
+	infos, err := f.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{
+		Hashes: []string{hash},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error to check torrent existence: %w", err)
+	}
+
+	if len(infos) == 0 {
+		return nil, nil
+	}
+
+	if len(infos) > 1 {
+		return nil, fmt.Errorf("multiple torrents with the same hash")
+	}
+
+	f.torrentsCache.Add(hash, *infos[0])
+
+	return infos[0], nil
+}
 
 func (f *cacheClient) getProperties(ctx context.Context, hash string) (*qbittorrent.TorrentProperties, error) {
+	if v, ok := f.propertiesCache.Get(hash); ok {
+		return &v, nil
+	}
+
 	info, err := f.qb.Torrent().GetProperties(ctx, hash)
 	if err != nil {
 		return nil, err
 	}
 
+	f.propertiesCache.Add(hash, *info)
 	return info, nil
 }
 
@@ -53,11 +104,20 @@ func (f *cacheClient) getContent(ctx context.Context, hash string, contentIndex
 }
 
 func (f *cacheClient) isPieceComplete(ctx context.Context, hash string, pieceIndex int) (bool, error) {
+	cachedPieceState, ok := f.pieceCache.Load(pieceKey{hash: hash, index: pieceIndex})
+	if ok && cachedPieceState == 2 {
+		return true, nil
+	}
+
 	completion, err := f.qb.Torrent().GetPiecesStates(ctx, hash)
 	if err != nil {
 		return false, err
 	}
 
+	for i, v := range completion {
+		f.pieceCache.Store(pieceKey{hash: hash, index: i}, v)
+	}
+
 	if completion[pieceIndex] == 2 {
 		return true, nil
 	}
diff --git a/src/sources/qbittorrent/daemon.go b/src/sources/qbittorrent/daemon.go
index 41fe7fa..777fe22 100644
--- a/src/sources/qbittorrent/daemon.go
+++ b/src/sources/qbittorrent/daemon.go
@@ -20,8 +20,11 @@ import (
 	"github.com/anacrolix/torrent/types/infohash"
 	infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
 	"github.com/royalcat/ctxio"
+	"go.opentelemetry.io/otel"
 )
 
+var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/sources/qbittorrent")
+
 type Daemon struct {
 	proc    *os.Process
 	qb      qbittorrent.Client
@@ -130,11 +133,14 @@ func (d *Daemon) Close(ctx context.Context) error {
 	return nil
 }
 
-func (d *Daemon) torrentPath(ih infohash.T) (string, error) {
-	return filepath.Abs(path.Join(d.dataDir, ih.HexString()))
+func torrentDataPath(dataDir string, ih string) (string, error) {
+	return filepath.Abs(path.Join(dataDir, ih))
 }
 
-func (fs *Daemon) TorrentFS(ctx context.Context, file vfs.File) (vfs.Filesystem, error) {
+func (fs *Daemon) GetTorrentFS(ctx context.Context, file vfs.File) (vfs.Filesystem, error) {
+	ctx, span := trace.Start(ctx, "GetTorrentFS")
+	defer span.End()
+
 	log := fs.log.With(slog.String("file", file.Name()))
 
 	ih, err := readInfoHash(ctx, file)
@@ -143,7 +149,7 @@ func (fs *Daemon) TorrentFS(ctx context.Context, file vfs.File) (vfs.Filesystem,
 	}
 	log = log.With(slog.String("infohash", ih.HexString()))
 
-	torrentPath, err := fs.torrentPath(ih)
+	torrentPath, err := torrentDataPath(fs.dataDir, ih.HexString())
 	if err != nil {
 		return nil, fmt.Errorf("error getting torrent path: %w", err)
 	}
@@ -160,18 +166,18 @@ func (fs *Daemon) TorrentFS(ctx context.Context, file vfs.File) (vfs.Filesystem,
 }
 
 func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainfo.Hash, torrentPath string) error {
+	ctx, span := trace.Start(ctx, "syncTorrentState")
+	defer span.End()
 	log := d.log.With(slog.String("file", file.Name()), slog.String("infohash", ih.HexString()))
 
-	existing, err := d.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{
-		Hashes: []string{ih.HexString()},
-	})
+	info, err := d.client.getInfo(ctx, ih.HexString())
 	if err != nil {
-		return fmt.Errorf("error to check torrent existence: %w", err)
+		return err
 	}
 
 	log = log.With(slog.String("torrentPath", torrentPath))
 
-	if len(existing) == 0 {
+	if info == nil {
 		_, err := file.Seek(0, io.SeekStart)
 		if err != nil {
 			return err
@@ -195,7 +201,7 @@ func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainf
 			return err
 		}
 		for {
-			_, err := d.qb.Torrent().GetProperties(ctx, ih.HexString())
+			_, err := d.client.getProperties(ctx, ih.HexString())
 			if err == nil {
 				break
 			}
@@ -211,9 +217,9 @@ func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainf
 		}
 
 		return nil
-	} else if len(existing) == 1 {
+	} else {
 		// info := existing[0]
-		props, err := d.qb.Torrent().GetProperties(ctx, ih.HexString())
+		props, err := d.client.getProperties(ctx, ih.HexString())
 		if err != nil {
 			return err
 		}
@@ -228,9 +234,6 @@ func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainf
 
 		return nil
 	}
-
-	return fmt.Errorf("multiple torrents with the same infohash")
-
 }
 
 // TODO caching
diff --git a/src/sources/qbittorrent/fs.go b/src/sources/qbittorrent/fs.go
index 7df8f8c..f2517ce 100644
--- a/src/sources/qbittorrent/fs.go
+++ b/src/sources/qbittorrent/fs.go
@@ -2,44 +2,62 @@ package qbittorrent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"io/fs"
+	"log/slog"
 	"os"
 	"path"
 	"strings"
+	"sync"
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
 	"git.kmsign.ru/royalcat/tstor/src/vfs"
 )
 
 type FS struct {
+	mu      sync.Mutex
 	client  *cacheClient
 	name    string
 	hash    string
-	dataDir string
+	dataDir string // directory where torrent files are stored
 
-	content map[string]*qbittorrent.TorrentContent
-	files   map[string]fs.FileInfo
+	entries map[string]fileEntry
+
+	log *rlog.Logger
 
 	vfs.FilesystemPrototype
 }
 
+type fileEntry struct {
+	fs.FileInfo
+	Content *qbittorrent.TorrentContent
+}
+
 var _ vfs.Filesystem = (*FS)(nil)
 
 func newTorrentFS(ctx context.Context, client *cacheClient, name string, hash string, dataDir string) (*FS, error) {
+	ctx, span := trace.Start(ctx, "newTorrentFS")
+	defer span.End()
+
 	cnts, err := client.listContent(ctx, hash)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list content for hash %s: %w", hash, err)
 	}
 
-	content := make(map[string]*qbittorrent.TorrentContent, len(cnts))
-	files := make(map[string]fs.FileInfo, len(cnts))
+	entries := make(map[string]fileEntry, len(cnts))
 	for _, cnt := range cnts {
-		path := vfs.AbsPath(cnt.Name)
-		files[path] = vfs.NewFileInfo(cnt.Name, cnt.Size)
-		content[path] = cnt
+		if cnt.Priority == qbittorrent.PriorityDoNotDownload {
+			continue
+		}
+
+		entries[vfs.AbsPath(cnt.Name)] = fileEntry{
+			Content:  cnt,
+			FileInfo: vfs.NewFileInfo(cnt.Name, cnt.Size),
+		}
 	}
 
 	return &FS{
@@ -49,8 +67,9 @@ func newTorrentFS(ctx context.Context, client *cacheClient, name string, hash st
 
 		dataDir: dataDir,
 
-		content: content,
-		files:   files,
+		entries: entries,
+
+		log: rlog.Component("qbittorrent", "fs"),
 
 		FilesystemPrototype: vfs.FilesystemPrototype(name),
 	}, nil
@@ -62,12 +81,11 @@ func (f *FS) Open(ctx context.Context, name string) (vfs.File, error) {
 		return vfs.NewDirFile(name), nil
 	}
 
-	cnt, ok := f.content[name]
-	if ok {
-		return openFile(ctx, f.client, f.dataDir, f.hash, cnt)
+	if entry, ok := f.entries[name]; ok {
+		return openFile(ctx, f.client, f.dataDir, f.hash, entry.Content)
 	}
 
-	for p := range f.content {
+	for p := range f.entries {
 		if strings.HasPrefix(p, name) {
 			return vfs.NewDirFile(name), nil
 		}
@@ -77,22 +95,79 @@ func (f *FS) Open(ctx context.Context, name string) (vfs.File, error) {
 }
 
 // ReadDir implements vfs.Filesystem.
-func (fs *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
-	return vfs.ListDirFromInfo(fs.files, name)
+func (f *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
+	infos := make(map[string]fs.FileInfo, len(f.entries))
+	for k, v := range f.entries {
+		infos[k] = v.FileInfo
+	}
+
+	return vfs.ListDirFromInfo(infos, name)
 }
 
 // Stat implements vfs.Filesystem.
 func (f *FS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
-	info, ok := f.files[name]
-	if !ok {
-		return nil, vfs.ErrNotExist
+	name = vfs.AbsPath(path.Clean(name))
+
+	if vfs.IsRoot(name) {
+		return vfs.NewDirInfo(f.name), nil
 	}
-	return info, nil
+
+	if entry, ok := f.entries[name]; ok {
+		return entry.FileInfo, nil
+	}
+
+	for p := range f.entries {
+		if strings.HasPrefix(p, name) {
+			return vfs.NewDirInfo(name), nil
+		}
+	}
+
+	return nil, vfs.ErrNotExist
 }
 
 // Unlink implements vfs.Filesystem.
 func (f *FS) Unlink(ctx context.Context, filename string) error {
-	return vfs.ErrNotImplemented
+	filename = vfs.AbsPath(path.Clean(filename))
+
+	// we cannot delete a torrent itself, cause it will be added on next source scan and all delited files will be restored
+
+	if entry, ok := f.entries[filename]; ok {
+		return f.removeFile(ctx, f.hash, entry.Content)
+	}
+
+	for p, entry := range f.entries {
+		if strings.HasPrefix(p, filename) {
+			return f.removeFile(ctx, f.hash, entry.Content)
+		}
+	}
+
+	return vfs.ErrNotExist
+}
+
+func (f *FS) removeFile(ctx context.Context, hash string, content *qbittorrent.TorrentContent) error {
+	log := f.log.With(slog.String("hash", hash), slog.String("file", content.Name))
+
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	fpath := vfs.AbsPath(content.Name)
+
+	if _, ok := f.entries[fpath]; !ok {
+		return fmt.Errorf("file %s is does not found", fpath)
+	}
+	delete(f.entries, fpath)
+
+	err := f.client.qb.Torrent().SetFilePriority(ctx, f.hash, content.Index, qbittorrent.PriorityDoNotDownload)
+	if err != nil {
+		return fmt.Errorf("failed to set priority for torrent %s for file %s: %w", hash, content.Name, err)
+	}
+
+	err = os.Remove(path.Join(f.dataDir, vfs.RelPath(content.Name)))
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		log.Warn(ctx, "failed to remove file", rlog.Error(err))
+		return fmt.Errorf("failed to remove file %s: %w", content.Name, err)
+	}
+	return nil
 }
 
 func openFile(ctx context.Context, client *cacheClient, torrentDir string, hash string, content *qbittorrent.TorrentContent) (*File, error) {
@@ -158,11 +233,73 @@ func (f *File) Name() string {
 	return path.Base(f.filePath)
 }
 
+func (f *File) canExpectSoon(ctx context.Context) (bool, error) {
+	info, err := f.client.getInfo(ctx, f.hash)
+	if err != nil {
+		return false, err
+	}
+
+	return info.Completed == info.Size || info.State == qbittorrent.TorrentStateCheckingUP || info.State == qbittorrent.TorrentStateDownloading || info.State == qbittorrent.TorrentStateForcedDL, nil
+}
+
+func (f *File) isRangeComplete(ctx context.Context, offset int64, size int) (bool, error) {
+	startPieceIndex := int(offset / int64(f.pieceSize))
+	pieceCount := (size + f.pieceSize - 1) / f.pieceSize // rouding up
+
+	for i := range pieceCount {
+		ok, err := f.client.isPieceComplete(ctx, f.hash, startPieceIndex+i)
+		if err != nil {
+			return false, err
+		}
+		if !ok {
+			return false, nil
+		}
+	}
+
+	return true, nil
+}
+
+func (f *File) waitPieceAvailable(ctx context.Context, offset int64, size int) error {
+	complete, err := f.isRangeComplete(ctx, offset, size)
+	if err != nil {
+		return err
+	}
+	if complete {
+		return nil
+	}
+
+	canExpectSoon, err := f.canExpectSoon(ctx)
+	if err != nil {
+		return err
+	}
+	if !canExpectSoon {
+		return fmt.Errorf("torrent is not downloading")
+	}
+
+	const checkingInterval = 1 * time.Second
+
+	ticker := time.NewTicker(checkingInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-ticker.C:
+			complete, err := f.isRangeComplete(ctx, offset, size)
+			if err != nil {
+				return err
+			}
+			if complete {
+				return nil
+			}
+		}
+	}
+}
+
 // Read implements vfs.File.
 func (f *File) Read(ctx context.Context, p []byte) (n int, err error) {
-	pieceIndex := int(f.offset / int64(f.pieceSize))
-	err = f.client.waitPieceToComplete(ctx, f.hash, pieceIndex)
-	if err != nil {
+	if err := f.waitPieceAvailable(ctx, f.offset, len(p)); err != nil {
 		return 0, err
 	}
 
@@ -178,9 +315,7 @@ func (f *File) Read(ctx context.Context, p []byte) (n int, err error) {
 
 // ReadAt implements vfs.File.
 func (f *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
-	pieceIndex := int(off / int64(f.pieceSize))
-	err = f.client.waitPieceToComplete(ctx, f.hash, pieceIndex)
-	if err != nil {
+	if err := f.waitPieceAvailable(ctx, f.offset, len(p)); err != nil {
 		return 0, err
 	}
 
diff --git a/src/sources/storage.go b/src/sources/storage.go
index 1420c7d..fe3b780 100644
--- a/src/sources/storage.go
+++ b/src/sources/storage.go
@@ -1,6 +1,8 @@
 package sources
 
 import (
+	"context"
+
 	"git.kmsign.ru/royalcat/tstor/src/sources/qbittorrent"
 	"git.kmsign.ru/royalcat/tstor/src/sources/ytdlp"
 	"git.kmsign.ru/royalcat/tstor/src/vfs"
@@ -8,7 +10,13 @@ import (
 
 func NewHostedFS(sourceFS vfs.Filesystem, tsrv *qbittorrent.Daemon, ytdlpsrv *ytdlp.Daemon) vfs.Filesystem {
 	factories := map[string]vfs.FsFactory{
-		".torrent":  tsrv.TorrentFS,
+		".torrent": func(ctx context.Context, f vfs.File) (vfs.Filesystem, error) {
+			tfs, err := tsrv.GetTorrentFS(ctx, f)
+			if err != nil {
+				return nil, err
+			}
+			return vfs.NewResolveFS(tfs, vfs.ArchiveFactories), nil
+		},
 		".ts-ytdlp": ytdlpsrv.BuildFS,
 	}
 
diff --git a/src/vfs/archive.go b/src/vfs/archive.go
index 47e69ec..c80c8b6 100644
--- a/src/vfs/archive.go
+++ b/src/vfs/archive.go
@@ -282,16 +282,17 @@ func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, e
 	}
 	n, err = d.buffer.ReadAt(p, off)
 	if err != nil && err != io.EOF {
-		return n, fmt.Errorf("failed to read from buffer: %w", err)
+		return n, fmt.Errorf("failed to readAt from buffer: %w", err)
 	}
 	return n, err
 }
 
 func (d *archiveFile) Close(ctx context.Context) error {
-	d.m.Lock()
-	defer d.m.Unlock()
+	return nil
+	// d.m.Lock()
+	// defer d.m.Unlock()
 
-	return d.buffer.Close()
+	// return d.buffer.Close()
 }
 
 type archiveFileReaderFactory func(ctx context.Context) (ctxio.ReadCloser, error)
diff --git a/src/vfs/hash.go b/src/vfs/hash.go
index 3bee0c2..092f373 100644
--- a/src/vfs/hash.go
+++ b/src/vfs/hash.go
@@ -15,6 +15,9 @@ var ErrOsHashLen = errors.New("oshash: buffer length must be a multiple of 8")
 type Hash string
 
 func FileHash(ctx context.Context, f File) (Hash, error) {
+	ctx, span := tracer.Start(ctx, "FileHash")
+	defer span.End()
+
 	_, err := f.Seek(0, io.SeekStart)
 	if err != nil {
 		return "", fmt.Errorf("error seeking file: %w", err)
diff --git a/src/vfs/log.go b/src/vfs/log.go
index 3f203bc..4491c5b 100644
--- a/src/vfs/log.go
+++ b/src/vfs/log.go
@@ -2,7 +2,9 @@ package vfs
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"io"
 	"io/fs"
 	"log/slog"
 	"reflect"
@@ -34,7 +36,7 @@ type LogFS struct {
 }
 
 func isLoggableError(err error) bool {
-	return err != nil // && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, io.EOF)
+	return err != nil && !errors.Is(err, io.EOF) // && !errors.Is(err, fs.ErrNotExist)
 }
 
 var _ Filesystem = (*LogFS)(nil)
@@ -169,10 +171,11 @@ func (lfs *LogFS) Stat(ctx context.Context, filename string) (info fs.FileInfo,
 		}
 		span.End()
 	}()
+	log := lfs.log.With(slog.String("filename", filename))
 
 	info, err = lfs.fs.Stat(ctx, filename)
 	if isLoggableError(err) {
-		lfs.log.Error(ctx, "Failed to stat", rlog.Error(err))
+		log.Error(ctx, "Failed to stat", rlog.Error(err))
 	}
 	return info, err
 }
@@ -190,10 +193,11 @@ func (fs *LogFS) Unlink(ctx context.Context, filename string) (err error) {
 		}
 		span.End()
 	}()
+	log := fs.log.With(slog.String("filename", filename))
 
 	err = fs.fs.Unlink(ctx, filename)
 	if isLoggableError(err) {
-		fs.log.Error(ctx, "Failed to stat", rlog.Error(err))
+		log.Error(ctx, "Failed to unlink", rlog.Error(err))
 	}
 	return err
 }
diff --git a/src/vfs/resolver.go b/src/vfs/resolver.go
index 3c697b6..5b87cdd 100644
--- a/src/vfs/resolver.go
+++ b/src/vfs/resolver.go
@@ -120,7 +120,7 @@ func (r *ResolverFS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, e
 				return nil, err
 			}
 			nestedfs, err := r.resolver.nestedFs(ctx, filepath, file)
-			if errors.Is(err, context.DeadlineExceeded) {
+			if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
 				return nil, err
 			}
 			if err != nil {
diff --git a/src/vfs/utils.go b/src/vfs/utils.go
index aed4003..739cbad 100644
--- a/src/vfs/utils.go
+++ b/src/vfs/utils.go
@@ -24,6 +24,10 @@ func trimRelPath(p, t string) string {
 // 	return path.Clean(Separator + strings.ReplaceAll(p, "\\", "/"))
 // }
 
+func RelPath(p string) string {
+	return strings.TrimLeft(p, Separator)
+}
+
 func AbsPath(p string) string {
 	if p == "" || p[0] != '/' {
 		return Separator + p