├── .dockerignore
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── github-actions-docker.yml
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build-all.sh
├── docker-all.sh
├── docker-entrypoint.sh
├── docker
├── Dockerfile
├── README.md
├── lite
│ ├── Dockerfile
│ ├── README.md
│ ├── cp.sh
│ └── makedocker.sh
└── start.sh
├── gen_web.go
├── installTorrServerLinux.sh
├── installTorrServerMac.sh
├── patches
├── 00-responsive-reader.patch
└── 01-no-udp-panic.patch
├── release.json
├── server
├── cmd
│ ├── TorrServer_windows_386.syso
│ ├── TorrServer_windows_amd64.syso
│ ├── main.go
│ ├── preconfig_and.go
│ ├── preconfig_pos.go
│ └── preconfig_win.go
├── dlna
│ ├── dlna.go
│ ├── list.go
│ └── utils.go
├── docs
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── ffprobe
│ └── ffprobe.go
├── go.mod
├── go.sum
├── log
│ └── log.go
├── mimetype
│ └── mimetype.go
├── rutor
│ ├── mem_test.go
│ ├── models
│ │ └── torrentDetails.go
│ ├── rutor.go
│ ├── torrsearch
│ │ ├── filter.go
│ │ ├── index.go
│ │ └── tokenizer.go
│ └── utils
│ │ └── utils.go
├── server.go
├── settings
│ ├── btsets.go
│ ├── db.go
│ ├── dbreadcache.go
│ ├── jsondb.go
│ ├── migrate.go
│ ├── settings.go
│ ├── torrent.go
│ ├── torrserverdb.go
│ ├── viewed.go
│ └── xpathdbrouter.go
├── tgbot
│ ├── add.go
│ ├── bot.go
│ ├── config
│ │ └── config.go
│ ├── delete.go
│ ├── files.go
│ ├── list.go
│ ├── upload.go
│ └── upload
│ │ ├── manager.go
│ │ ├── queue.go
│ │ └── torrfile.go
├── torr
│ ├── apihelper.go
│ ├── btserver.go
│ ├── dbwrapper.go
│ ├── preload.go
│ ├── state
│ │ └── state.go
│ ├── storage
│ │ ├── state
│ │ │ └── state.go
│ │ ├── storage.go
│ │ └── torrstor
│ │ │ ├── cache.go
│ │ │ ├── diskpiece.go
│ │ │ ├── mempiece.go
│ │ │ ├── piece.go
│ │ │ ├── piecefake.go
│ │ │ ├── ranges.go
│ │ │ ├── reader.go
│ │ │ └── storage.go
│ ├── stream.go
│ ├── torrent.go
│ └── utils
│ │ ├── blockedIP.go
│ │ ├── freemem.go
│ │ ├── torrent.go
│ │ └── webImageChecker.go
├── utils
│ ├── filetypes.go
│ ├── location.go
│ ├── prallel.go
│ └── strings.go
├── version
│ └── version.go
└── web
│ ├── api
│ ├── cache.go
│ ├── download.go
│ ├── ffprobe.go
│ ├── m3u.go
│ ├── play.go
│ ├── route.go
│ ├── rutor.go
│ ├── settings.go
│ ├── shutdown.go
│ ├── stream.go
│ ├── torrents.go
│ ├── upload.go
│ ├── utils
│ │ └── link.go
│ └── viewed.go
│ ├── auth
│ └── auth.go
│ ├── blocker
│ ├── blocker.go
│ └── iplist.go
│ ├── msx
│ └── msx.go
│ ├── pages
│ ├── route.go
│ └── template
│ │ ├── html.go
│ │ ├── pages
│ │ ├── apple-splash-1125-2436.jpg
│ │ ├── apple-splash-1136-640.jpg
│ │ ├── apple-splash-1170-2532.jpg
│ │ ├── apple-splash-1242-2208.jpg
│ │ ├── apple-splash-1242-2688.jpg
│ │ ├── apple-splash-1284-2778.jpg
│ │ ├── apple-splash-1334-750.jpg
│ │ ├── apple-splash-1536-2048.jpg
│ │ ├── apple-splash-1620-2160.jpg
│ │ ├── apple-splash-1668-2224.jpg
│ │ ├── apple-splash-1668-2388.jpg
│ │ ├── apple-splash-1792-828.jpg
│ │ ├── apple-splash-2048-1536.jpg
│ │ ├── apple-splash-2048-2732.jpg
│ │ ├── apple-splash-2160-1620.jpg
│ │ ├── apple-splash-2208-1242.jpg
│ │ ├── apple-splash-2224-1668.jpg
│ │ ├── apple-splash-2388-1668.jpg
│ │ ├── apple-splash-2436-1125.jpg
│ │ ├── apple-splash-2532-1170.jpg
│ │ ├── apple-splash-2688-1242.jpg
│ │ ├── apple-splash-2732-2048.jpg
│ │ ├── apple-splash-2778-1284.jpg
│ │ ├── apple-splash-640-1136.jpg
│ │ ├── apple-splash-750-1334.jpg
│ │ ├── apple-splash-828-1792.jpg
│ │ ├── asset-manifest.json
│ │ ├── browserconfig.xml
│ │ ├── dlnaicon-120.png
│ │ ├── dlnaicon-48.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── icon.png
│ │ ├── index.html
│ │ ├── logo.png
│ │ ├── mstile-150x150.png
│ │ ├── site.webmanifest
│ │ └── static
│ │ │ └── js
│ │ │ ├── 2.292d2615.chunk.js
│ │ │ ├── 2.292d2615.chunk.js.LICENSE.txt
│ │ │ ├── 2.292d2615.chunk.js.map
│ │ │ ├── main.88d118ab.chunk.js
│ │ │ ├── main.88d118ab.chunk.js.map
│ │ │ ├── runtime-main.5ed86a79.js
│ │ │ └── runtime-main.5ed86a79.js.map
│ │ └── route.go
│ ├── server.go
│ └── sslcerts
│ └── sslcerts.go
├── torrserver.service
├── upx
└── web
├── .babelrc
├── .env_example
├── .eslintrc
├── .gitignore
├── README.md
├── jsconfig.json
├── package.json
├── public
├── apple-splash-1125-2436.jpg
├── apple-splash-1136-640.jpg
├── apple-splash-1170-2532.jpg
├── apple-splash-1242-2208.jpg
├── apple-splash-1242-2688.jpg
├── apple-splash-1284-2778.jpg
├── apple-splash-1334-750.jpg
├── apple-splash-1536-2048.jpg
├── apple-splash-1620-2160.jpg
├── apple-splash-1668-2224.jpg
├── apple-splash-1668-2388.jpg
├── apple-splash-1792-828.jpg
├── apple-splash-2048-1536.jpg
├── apple-splash-2048-2732.jpg
├── apple-splash-2160-1620.jpg
├── apple-splash-2208-1242.jpg
├── apple-splash-2224-1668.jpg
├── apple-splash-2388-1668.jpg
├── apple-splash-2436-1125.jpg
├── apple-splash-2532-1170.jpg
├── apple-splash-2688-1242.jpg
├── apple-splash-2732-2048.jpg
├── apple-splash-2778-1284.jpg
├── apple-splash-640-1136.jpg
├── apple-splash-750-1334.jpg
├── apple-splash-828-1792.jpg
├── browserconfig.xml
├── dlnaicon-120.png
├── dlnaicon-48.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── icon.png
├── index.html
├── logo.png
├── mstile-150x150.png
└── site.webmanifest
├── src
├── components
│ ├── About
│ │ ├── LinkComponent.jsx
│ │ ├── index.jsx
│ │ └── style.js
│ ├── Add
│ │ ├── AddDialog.jsx
│ │ ├── LeftSideComponent.jsx
│ │ ├── RightSideComponent.jsx
│ │ ├── helpers.js
│ │ ├── index.jsx
│ │ └── style.js
│ ├── App
│ │ ├── PWAFooter
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── PWAInstallationGuide
│ │ │ ├── IOSShareIcon.jsx
│ │ │ ├── index.jsx
│ │ │ └── style.jsx
│ │ ├── Sidebar.jsx
│ │ ├── index.jsx
│ │ └── style.js
│ ├── CloseServer.jsx
│ ├── DialogTorrentDetailsContent
│ │ ├── DetailedView
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── DialogHeader.jsx
│ │ ├── StatisticsField.jsx
│ │ ├── Table
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── TorrentCache
│ │ │ ├── getShortCacheMap.js
│ │ │ ├── index.jsx
│ │ │ ├── snakeSettings.js
│ │ │ └── style.js
│ │ ├── TorrentFunctions
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── customHooks.jsx
│ │ ├── helpers.js
│ │ ├── index.jsx
│ │ ├── style.js
│ │ └── widgets
│ │ │ ├── index.jsx
│ │ │ └── useGetWidgetColors.jsx
│ ├── Donate
│ │ ├── DonateDialog.jsx
│ │ └── index.jsx
│ ├── FilterByCategory.jsx
│ ├── RemoveAll.jsx
│ ├── Settings
│ │ ├── MobileAppSettings.jsx
│ │ ├── PrimarySettingsComponent.jsx
│ │ ├── SecondarySettingsComponent.jsx
│ │ ├── SettingsDialog.jsx
│ │ ├── SliderInput.jsx
│ │ ├── defaultSettings.js
│ │ ├── index.jsx
│ │ ├── style.js
│ │ └── tabComponents.jsx
│ ├── TorrentCard
│ │ ├── index.jsx
│ │ └── style.js
│ ├── TorrentList
│ │ ├── AddFirstTorrent.jsx
│ │ ├── NoServerConnection.jsx
│ │ ├── index.jsx
│ │ └── style.js
│ ├── UnsafeButton.jsx
│ └── categories.jsx
├── i18n.js
├── icons
│ └── index.jsx
├── index.jsx
├── locales
│ ├── bg
│ │ └── translation.json
│ ├── en
│ │ └── translation.json
│ ├── ru
│ │ └── translation.json
│ ├── ua
│ │ └── translation.json
│ └── zh
│ │ └── translation.json
├── style
│ ├── CustomMaterialUiStyles.js
│ ├── DialogStyles.js
│ ├── GlobalStyle.js
│ ├── colors.js
│ ├── getStyledComponentsTheme.js
│ ├── materialUISetup.js
│ └── standaloneMedia.js
├── torrentStates.js
└── utils
│ ├── Hosts.js
│ ├── Utils.js
│ ├── checkIsIOS.jsx
│ ├── useChangeLanguage.js
│ ├── useOnStandaloneAppOutsideClick.jsx
│ └── usePreviousState.js
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | web/node_modules
2 | web/build
3 | web/dist
4 | web/.env_example
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. Linux Ubuntu 22.04]
28 | - TorrServer Version [e.g. MatriX.129]
29 |
30 | **Smartphone or tvbox on Android (please complete the following information):**
31 | - Device: [e.g. Ugoos am6]
32 | - OS: [e.g. Android 9]
33 | - TorrServer Version [e.g. MatriX.129]
34 |
35 | **Additional context**
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[Feature]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/github-actions-docker.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | docker:
9 | runs-on: ubuntu-latest
10 | steps:
11 | -
12 | name: tag number
13 | run : echo ${{ github.event.release.tag_name }}
14 | -
15 | name: Checkout
16 | uses: actions/checkout@v3.3.0
17 | -
18 | name: Set up QEMU
19 | uses: docker/setup-qemu-action@v2.1.0
20 | -
21 | name: Set up Docker Buildx
22 | uses: docker/setup-buildx-action@v2.2.1
23 | -
24 | name: Login to GitHub Container Registry
25 | uses: docker/login-action@v2.1.0
26 | with:
27 | registry: ghcr.io
28 | username: ${{ github.actor }}
29 | password: ${{ secrets.GITHUB_TOKEN }}
30 | -
31 | name: set lower case owner name
32 | run: |
33 | echo "REG_REPO=${REPO,,}" >>${GITHUB_ENV}
34 | env:
35 | REPO: '${{ github.repository }}'
36 | -
37 | name: CHECK ENVS
38 | run: |
39 | echo ${{ env.REG_REPO }}
40 | -
41 | name: Build and push
42 | uses: docker/build-push-action@v3.3.0
43 | with:
44 | context: .
45 | platforms: linux/amd64,linux/arm/v7,linux/arm64
46 | push: true
47 | tags: |
48 | ghcr.io/${{ env.REG_REPO }}:${{ github.event.release.tag_name }}
49 | ghcr.io/${{ env.REG_REPO }}:latest
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Mac suff
9 | .DS_Store
10 |
11 | # Test binary, build with `go test -c`
12 | *.test
13 |
14 | # Mac stuff
15 | .DS_Store
16 |
17 | # Output of the go coverage tool, specifically when used with LiteIDE
18 | *.out
19 |
20 | TorrServer.iml
21 | .idea/
22 | .vscode
23 |
24 | src/github.com/
25 | src/golang.org/
26 | src/bazil.org/
27 | src/gopkg.in/
28 | src/go.opencensus.io/
29 | pkg/
30 | bin/
31 | dist/
32 | toolchains/
33 | /src/crawshaw.io/
34 | /src/go.etcd.io/
35 | /src/google.golang.org/
36 | /toolchain/
37 | /server/web/pages/template/pages/msx
38 |
39 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - build_go
3 |
4 | build_go:
5 | image: golang:latest
6 | stage: build_go
7 | when: manual
8 | tags:
9 | - amd64
10 | artifacts:
11 | name: "TorrServer"
12 | paths:
13 | - dist
14 | script:
15 | - apt update
16 | - apt install -y npm zip
17 | - rm -rf /var/lib/apt/lists/*
18 | - npm install -g yarn
19 | - wget -q "https://dl.google.com/android/repository/android-ndk-r25c-linux.zip"
20 | - unzip ./android-ndk-r25c-linux.zip
21 | - rm ./android-ndk-r25c-linux.zip
22 | - pwd
23 | - ls -l
24 | - ./build-all.sh
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ### FRONT BUILD START ###
2 | FROM --platform=$BUILDPLATFORM node:16-alpine AS front
3 |
4 | WORKDIR /app
5 |
6 | ARG REACT_APP_SERVER_HOST='.'
7 | ARG REACT_APP_TMDB_API_KEY=''
8 | ARG PUBLIC_URL=''
9 |
10 | ENV REACT_APP_SERVER_HOST=$REACT_APP_SERVER_HOST
11 | ENV REACT_APP_TMDB_API_KEY=$REACT_APP_TMDB_API_KEY
12 | ENV PUBLIC_URL=$PUBLIC_URL
13 |
14 | COPY ./web/package.json ./web/yarn.lock ./
15 | RUN yarn install
16 |
17 | # Build front once upon multiarch build
18 | COPY ./web .
19 | RUN yarn run build
20 | ### FRONT BUILD END ###
21 |
22 |
23 | ### BUILD TORRSERVER MULTIARCH START ###
24 | FROM --platform=$BUILDPLATFORM golang:1.24.0-alpine AS builder
25 |
26 | COPY . /opt/src
27 | COPY --from=front /app/build /opt/src/web/build
28 |
29 | WORKDIR /opt/src
30 |
31 | ARG TARGETARCH
32 |
33 | # Step for multiarch build with docker buildx
34 | ENV GOARCH=$TARGETARCH
35 |
36 | # Build torrserver
37 | RUN apk add --update g++ \
38 | && go run gen_web.go \
39 | && cd server \
40 | && go mod tidy \
41 | && go clean -i -r -cache \
42 | && go build -ldflags '-w -s' --o "torrserver" ./cmd
43 | ### BUILD TORRSERVER MULTIARCH END ###
44 |
45 |
46 | ### UPX COMPRESSING START ###
47 | FROM debian:buster-slim AS compressed
48 |
49 | COPY --from=builder /opt/src/server/torrserver ./torrserver
50 |
51 | RUN apt-get update && apt-get install -y upx-ucl && upx --best --lzma ./torrserver
52 | # Compress torrserver only for amd64 and arm64 no variant platforms
53 | # ARG TARGETARCH
54 | # ARG TARGETVARIANT
55 | # RUN if [ "$TARGETARCH" == 'amd64' ]; then compress=1; elif [ "$TARGETARCH" == 'arm64' ] && [ -z "$TARGETVARIANT" ]; then compress=1; else compress=0; fi \
56 | # && if [[ "$compress" -eq 1 ]]; then ./upx --best --lzma ./torrserver; fi
57 | ### UPX COMPRESSING END ###
58 |
59 |
60 | ### BUILD MAIN IMAGE START ###
61 | FROM alpine
62 |
63 | ENV TS_CONF_PATH="/opt/ts/config"
64 | ENV TS_LOG_PATH="/opt/ts/log"
65 | ENV TS_TORR_DIR="/opt/ts/torrents"
66 | ENV TS_PORT=8090
67 | ENV GODEBUG=madvdontneed=1
68 |
69 | COPY --from=compressed ./torrserver /usr/bin/torrserver
70 | COPY ./docker-entrypoint.sh /docker-entrypoint.sh
71 |
72 | RUN apk add --no-cache --update ffmpeg
73 |
74 | CMD /docker-entrypoint.sh
75 | ### BUILD MAIN IMAGE end ###
76 |
--------------------------------------------------------------------------------
/docker-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT=${PWD}
4 |
5 | #### Build web
6 | echo "Build web"
7 | go run gen_web.go
8 |
9 | sudo docker run --rm -v "$PWD":/usr/src/torr -v ~/go/pkg/mod:/go/pkg/mod -w /usr/src/torr golang:1.17.5-stretch ./build-all.sh
10 | sudo chmod 0777 ./dist/*
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | FLAGS="--path $TS_CONF_PATH --logpath $TS_LOG_PATH --port $TS_PORT --torrentsdir $TS_TORR_DIR"
4 | if [[ "$TS_HTTPAUTH" -eq 1 ]]; then FLAGS="${FLAGS} --httpauth"; fi
5 | if [[ "$TS_RDB" -eq 1 ]]; then FLAGS="${FLAGS} --rdb"; fi
6 | if [[ "$TS_DONTKILL" -eq 1 ]]; then FLAGS="${FLAGS} --dontkill"; fi
7 | if [[ "$TS_EN_SSL" -eq 1 ]]; then FLAGS="${FLAGS} --ssl"; fi
8 | if [[ -v "$TS_SSL_PORT" ]]; then FLAGS="${FLAGS} --sslport ${TS_SSL_PORT}"; fi
9 |
10 |
11 | if [ ! -d $TS_CONF_PATH ]; then
12 | mkdir -p $TS_CONF_PATH
13 | fi
14 |
15 | if [ ! -d $TS_TORR_DIR ]; then
16 | mkdir -p $TS_TORR_DIR
17 | fi
18 |
19 | if [ ! -f $TS_LOG_PATH ]; then
20 | touch $TS_LOG_PATH
21 | fi
22 |
23 | echo "Running with: ${FLAGS}"
24 |
25 | torrserver $FLAGS
26 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine
2 | LABEL maintainer "yourok"
3 | RUN apk add --no-cache wget
4 | COPY start.sh /start.sh
5 | ENTRYPOINT /start.sh
6 |
7 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | ## TorrServer
2 |
3 | After starting the container, the latest server is downloaded from GitHub.\
4 | If you need update server to latest, repull container
5 |
6 | Source code: https://github.com/YouROK/TorrServer
7 |
8 | --------
9 |
10 | Author of docker file and scripts [butaford (aka Pavel)](https://github.com/butaford)
11 |
12 | --------
13 |
14 | ### Support platforms
15 | * TorrServer-linux-386
16 | * TorrServer-linux-amd64
17 | * TorrServer-linux-arm5
18 | * TorrServer-linux-arm64
19 | * TorrServer-linux-arm7
20 |
21 | --------
22 | ### Support env
23 | TS_PORT: TS web port\
24 | TS_PATH: config path and other\
25 | TS_LOGPATHDIR: log path\
26 | TS_LOGFILE: log file name\
27 | TS_WEBLOGFILE: web log file name\
28 | TS_RDB: read only config\
29 | TS_HTTPAUTH: auth for server, accs.db should be in the TS_PATH\
30 | TS_DONTKILL: don't kill server by signal\
31 | TS_TORRENTSDIR: torrents listen directory\
32 | TS_TORRENTADDR: torrents peer listen port\
33 | TS_PUBIPV4: the IP addresses as our peers should see them. May differ from the local interfaces due to NAT or other network configurations\
34 | TS_PUBIPV6: the IP addresses as our peers should see them. May differ from the local interfaces due to NAT or other network configurations\
35 | TS_SEARCHWA: disable auth for search torrents if auth is enable
36 |
37 | --------
38 | ### Docker run example
39 | ```
40 | docker run -p 8090:8090 \
41 | -e TS_PORT=8090 \
42 | -e TS_PATH="/opt/torrserver/config" \
43 | -e TS_LOGPATHDIR="/opt/torrserver/log/" \
44 | -e TS_LOGFILE="ts.log" \
45 | -e TS_WEBLOGFILE="tsweb.log" \
46 | -e TS_RDB=true \
47 | -e TS_HTTPAUTH=true \
48 | -e TS_DONTKILL=true \
49 | -e TS_TORRENTSDIR="/opt/torrserver/torrents" \
50 | -e TS_TORRENTADDR=32000 \
51 | -e TS_PUBIPV4=publicIP \
52 | -e TS_PUBIPV6=publicIP \
53 | -e TS_SEARCHWA=true \
54 | yourok/torrserver
55 | ```
56 |
57 | --------
58 | ### Docker compose example
59 | ```
60 | version: '3.6'
61 | services:
62 | torrserver:
63 | container_name: torrserver
64 | image: yourok/torrserver
65 | restart: unless-stopped
66 | environment:
67 | - TS_PORT=8090
68 | - TS_PATH=/opt/torrserver/config
69 | - TS_LOGPATHDIR=/opt/torrserver/log
70 | - TS_LOGFILE=ts.log
71 | - TS_WEBLOGFILE=tsweb.log
72 | - TS_RDB=false
73 | - TS_HTTPAUTH=true
74 | - TS_DONTKILL=true
75 | - TS_TORRENTSDIR=/opt/torrserver/torrents
76 | - TS_TORRENTADDR=:32000
77 | - TS_PUBIPV4=publicIP
78 | - TS_PUBIPV6=publicIP
79 | - TS_SEARCHWA=true
80 | ports:
81 | - 8090:8090
82 | volumes:
83 | - ./torrserver/config:/opt/torrserver/config
84 | - ./torrserver/log:/opt/torrserver/log
85 | - ./torrserver/torrents:/opt/torrserver/torrents
86 | ```
87 |
--------------------------------------------------------------------------------
/docker/lite/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm-slim as builder
2 | RUN mkdir /src
3 | COPY ./ /src
4 | RUN /src/cp.sh
5 |
6 | FROM scratch
7 | COPY --from=builder /app/TorrServer /
8 | WORKDIR /
9 | ENTRYPOINT [ "/TorrServer" ]
10 |
--------------------------------------------------------------------------------
/docker/lite/README.md:
--------------------------------------------------------------------------------
1 | ## TorrServer
2 |
3 | A lightweight container that contains a single TorrServer file
4 |
5 | Source code: https://github.com/YouROK/TorrServer
6 |
7 | --------
8 |
9 | ### Support platforms
10 | * TorrServer-linux-386
11 | * TorrServer-linux-amd64
12 | * TorrServer-linux-arm5
13 | * TorrServer-linux-arm64
14 | * TorrServer-linux-arm7
15 |
16 | --------
17 | ### Docker run example
18 | ```
19 | docker run -p 8090:8090 yourok/torrlite:TAG [ ARGS ]
20 | ```
21 |
22 | TAG - tag of version in docker hub eg MatriX.134 \
23 | ARGS - args of torrserver
24 |
25 | You can mount a directory like -v /your/local/path/:/cfg and write logs etc there
26 |
27 | Example of run with args:
28 | ```
29 | docker run -p 8099:8099 yourok/torrlite:MatriX.134 --port=8099
30 | ```
--------------------------------------------------------------------------------
/docker/lite/cp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | case $(uname -m) in
4 | i386) architecture="386" ;;
5 | i686) architecture="386" ;;
6 | x86_64) architecture="amd64" ;;
7 | aarch64) architecture="arm64" ;;
8 | armv7|armv7l) architecture="arm7" ;;
9 | armv6|armv6l) architecture="arm5" ;;
10 | # armv5|armv5l) architecture="arm5" ;;
11 | *) echo "Unsupported Arch. Can't continue."; exit 1 ;;
12 | esac
13 |
14 | binName="TorrServer-linux-${architecture}"
15 | mkdir -p /app
16 |
17 | cp /src/dist/$binName /app/TorrServer
--------------------------------------------------------------------------------
/docker/lite/makedocker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cp -r ../../dist ./
3 | docker buildx build --platform "linux/386,linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6" --tag yourok/torrlite:$* --push .
4 | rm -rf ./dist
--------------------------------------------------------------------------------
/docker/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | case $(uname -m) in
4 | i386) architecture="386" ;;
5 | i686) architecture="386" ;;
6 | x86_64) architecture="amd64" ;;
7 | aarch64) architecture="arm64" ;;
8 | armv7|armv7l) architecture="arm7" ;;
9 | armv6|armv6l) architecture="arm5" ;;
10 | # armv5|armv5l) architecture="arm5" ;;
11 | *) echo "Unsupported Arch. Can't continue."; exit 1 ;;
12 | esac
13 |
14 | binName="TorrServer-linux-${architecture}"
15 |
16 | mkdir -p /opt/torrserver
17 | cd /opt/torrserver
18 |
19 | rm -f ${binName}*
20 |
21 | wget -O $binName "https://github.com/YouROK/TorrServer/releases/latest/download/$binName"
22 | chmod +x $binName
23 |
24 | FLAGS=""
25 |
26 | #sets start flags
27 | [ ! -z "$TS_PORT" ] && echo "TS_PORT: $TS_PORT" && FLAGS="${FLAGS} --port ${TS_PORT}"
28 | [ ! -z "$TS_PATH" ] && echo "TS_PATH: $TS_PATH" && FLAGS="${FLAGS} --path ${TS_PATH}"
29 | [ ! -z "$TS_LOGPATHDIR" ] && echo "TS_LOGPATHDIR: $TS_LOGPATHDIR" && FLAGS="${FLAGS}"
30 | [ ! -z "$TS_LOGFILE" ] && echo "TS_LOGFILE: $TS_LOGPATHDIR/$TS_LOGFILE" && FLAGS="${FLAGS} --logpath $TS_LOGPATHDIR/${TS_LOGFILE}"
31 | [ ! -z "$TS_WEBLOGFILE" ] && echo "TS_WEBLOGFILE: $TS_LOGPATHDIR/$TS_WEBLOGFILE" && FLAGS="${FLAGS} --weblogpath $TS_LOGPATHDIR/${TS_WEBLOGFILE}"
32 | [ ! -z "$TS_RDB" ] | [ "$TS_RDB" = "true" ] && echo "TS_RDB: $TS_RDB" && FLAGS="${FLAGS} --rdb"
33 | [ ! -z "$TS_HTTPAUTH" ] && echo "TS_HTTPAUTH: $TS_HTTPAUTH" && FLAGS="${FLAGS} --httpauth"
34 | [ ! -z "$TS_DONTKILL" ] && echo "TS_DONTKILL: $TS_DONTKILL" && FLAGS="${FLAGS} --dontkill"
35 | [ ! -z "$TS_TORRENTSDIR" ] && echo "TS_TORRENTSDIR: $TS_TORRENTSDIR" && FLAGS="${FLAGS} --torrentsdir ${TS_TORRENTSDIR}"
36 | [ ! -z "$TS_TORRENTADDR" ] && echo "TS_TORRENTADDR: $TS_TORRENTADDR" && FLAGS="${FLAGS} --torrentaddr ${TS_TORRENTADDR}"
37 | [ ! -z "$TS_PUBIPV4" ] && echo "TS_PUBIPV4: $TS_PUBIPV4" && FLAGS="${FLAGS} --pubipv4 ${TS_PUBIPV4}"
38 | [ ! -z "$TS_PUBIPV6" ] && echo "TS_PUBIPV6: $TS_PUBIPV6" && FLAGS="${FLAGS} --pubipv6 ${TS_PUBIPV6}"
39 | [ ! -z "$TS_SEARCHWA" ]&& echo "TS_SEARCHWA: $TS_SEARCHWA" && FLAGS="${FLAGS} --searchwa"
40 |
41 | #make directories
42 | [ ! -z "$TS_PATH" ] && [ ! -d "$TS_PATH" ] && mkdir -p $TS_PATH
43 | [ ! -z "$TS_LOGPATHDIR" ] && [ ! -d "$TS_LOGPATHDIR" ] && mkdir -p $TS_LOGPATHDIR
44 | [ ! -z "$TS_TORRENTSDIR" ] && [ ! -d "$TS_TORRENTSDIR" ] && mkdir $TS_TORRENTSDIR
45 |
46 | echo "Running with: ${FLAGS}"
47 | export GODEBUG=madvdontneed=1
48 |
49 | /opt/torrserver/${binName} ${FLAGS}
50 |
--------------------------------------------------------------------------------
/patches/00-responsive-reader.patch:
--------------------------------------------------------------------------------
1 | --- reader.go.orig 2022-06-01 09:26:35.000000000 +0300
2 | +++ reader.go 2022-05-17 05:47:36.000000000 +0300
3 | @@ -102,9 +102,9 @@
4 | if !ok {
5 | break
6 | }
7 | - if !r.responsive && !r.t.pieceComplete(pieceIndex(req.Index)) {
8 | - break
9 | - }
10 | +// if !r.responsive && !r.t.pieceComplete(pieceIndex(req.Index)) {
11 | +// break
12 | +// }
13 | if !r.t.haveChunk(req) {
14 | break
15 | }
16 |
--------------------------------------------------------------------------------
/patches/01-no-udp-panic.patch:
--------------------------------------------------------------------------------
1 | --- tracker/udp/conn-client.go.orig 2022-05-16 05:10:10.000000000 +0300
2 | +++ tracker/udp/conn-client.go 2022-06-01 09:34:27.000000000 +0300
3 | @@ -38,9 +38,9 @@
4 | // TODO: Do bad things to the dispatcher, and incoming calls to the client if we have a
5 | // read error.
6 | cc.readErr = err
7 | - if !cc.closed {
8 | - panic(err)
9 | - }
10 | +// if !cc.closed {
11 | +// panic(err)
12 | +// }
13 | break
14 | }
15 | err = cc.d.Dispatch(b[:n], addr)
16 |
--------------------------------------------------------------------------------
/release.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "TorrServer",
3 | "Version": "1.1.77",
4 | "BuildDate": "05.08.2020",
5 | "Links": {
6 | "android-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-386",
7 | "android-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-amd64",
8 | "android-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm64",
9 | "android-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm7",
10 | "android-10-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm64-10",
11 | "android-10-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm7-10",
12 | "darwin-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-darwin-amd64",
13 | "freebsd-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-freebsd-amd64",
14 | "linux-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-386",
15 | "linux-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-amd64",
16 | "linux-arm5": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm5",
17 | "linux-arm6": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm6",
18 | "linux-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm64",
19 | "linux-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm7",
20 | "linux-mips": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips",
21 | "linux-mips64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips64",
22 | "linux-mips64le": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips64le",
23 | "linux-mipsle": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mipsle",
24 | "windows-386.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-windows-386.exe",
25 | "windows-amd64.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-windows-amd64.exe"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/cmd/TorrServer_windows_386.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/cmd/TorrServer_windows_386.syso
--------------------------------------------------------------------------------
/server/cmd/TorrServer_windows_amd64.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/cmd/TorrServer_windows_amd64.syso
--------------------------------------------------------------------------------
/server/cmd/preconfig_and.go:
--------------------------------------------------------------------------------
1 | //go:build android
2 | // +build android
3 |
4 | package main
5 |
6 | // #cgo LDFLAGS: -static-libstdc++
7 | import "C"
8 |
--------------------------------------------------------------------------------
/server/cmd/preconfig_pos.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package main
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "syscall"
10 |
11 | "server/log"
12 | "server/settings"
13 | )
14 |
15 | func Preconfig(dkill bool) {
16 | if dkill {
17 | sigc := make(chan os.Signal, 1)
18 | signal.Notify(sigc,
19 | syscall.SIGHUP,
20 | syscall.SIGINT,
21 | syscall.SIGPIPE,
22 | syscall.SIGTERM,
23 | syscall.SIGQUIT)
24 | go func() {
25 | for s := range sigc {
26 | if dkill {
27 | if settings.BTsets.EnableDebug || s != syscall.SIGPIPE {
28 | log.TLogln("Signal catched:", s)
29 | log.TLogln("To stop server, close it from web / api")
30 | }
31 | }
32 | }
33 | }()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/cmd/preconfig_win.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package main
5 |
6 | import (
7 | "runtime"
8 | "syscall"
9 | "time"
10 |
11 | "server/torr"
12 | "server/torr/state"
13 | )
14 |
15 | const (
16 | EsSystemRequired = 0x00000001
17 | EsAwaymodeRequired = 0x00000040 // Added for future improvements
18 | EsContinuous = 0x80000000
19 | )
20 |
21 | var (
22 | pulseTime = 60 * time.Second
23 | clearFlagTimeout = 3 * 60 * time.Second
24 | )
25 |
26 | func Preconfig(kill bool) {
27 | go func() {
28 | // need work on one thread because SetThreadExecutionState sets flag to thread. We need set and clear flag for same thread.
29 | runtime.LockOSThread()
30 | // don't sleep/hibernate windows
31 | kernel32 := syscall.NewLazyDLL("kernel32.dll")
32 | setThreadExecStateProc := kernel32.NewProc("SetThreadExecutionState")
33 | currentExecState := uintptr(EsContinuous)
34 | normalExecutionState := uintptr(EsContinuous)
35 | systemRequireState := uintptr(EsSystemRequired | EsContinuous)
36 | pulse := time.NewTicker(pulseTime)
37 | var clearFlagTime int64 = -1
38 | for {
39 | select {
40 | case <-pulse.C:
41 | {
42 | systemRequired := false
43 | for _, torrent := range torr.ListTorrent() {
44 | if torrent.Stat != state.TorrentInDB {
45 | systemRequired = true
46 | break
47 | }
48 | }
49 | if systemRequired && currentExecState != systemRequireState {
50 | // Looks like sending just EsSystemRequired to clear timer is broken in Win11.
51 | // Enable system required to avoid the system to idle to sleep.
52 | currentExecState = systemRequireState
53 | setThreadExecStateProc.Call(systemRequireState)
54 | }
55 |
56 | if !systemRequired && currentExecState != normalExecutionState {
57 | // Clear EXECUTION_STATE flags to disable away mode and allow the system to idle to sleep normally.
58 |
59 | // Avoid clear flag immediately to add time to start next episode
60 | if clearFlagTime == -1 {
61 | clearFlagTime = time.Now().Unix() + int64(clearFlagTimeout.Seconds())
62 | }
63 |
64 | if clearFlagTime >= time.Now().Unix() {
65 | clearFlagTime = -1
66 | currentExecState = normalExecutionState
67 | setThreadExecStateProc.Call(normalExecutionState)
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }()
74 | }
75 |
--------------------------------------------------------------------------------
/server/dlna/utils.go:
--------------------------------------------------------------------------------
1 | package dlna
2 |
3 | import (
4 | "path/filepath"
5 | )
6 |
7 | func isHashPath(path string) bool {
8 | base := filepath.Base(path)
9 | if len(base) == 40 {
10 | data := []byte(base)
11 | for _, v := range data {
12 | if !(v >= 48 && v <= 57 || v >= 65 && v <= 70 || v >= 97 && v <= 102) {
13 | return false
14 | }
15 | }
16 | return true
17 | }
18 | return false
19 | }
20 |
--------------------------------------------------------------------------------
/server/ffprobe/ffprobe.go:
--------------------------------------------------------------------------------
1 | package ffprobe
2 |
3 | import (
4 | "context"
5 | "io"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "time"
10 |
11 | "gopkg.in/vansante/go-ffprobe.v2"
12 | )
13 |
14 | var binFile = "ffprobe"
15 |
16 | func init() {
17 | path, err := exec.LookPath("ffprobe")
18 | if err == nil {
19 | ffprobe.SetFFProbeBinPath(path)
20 | binFile = path
21 | } else {
22 | // working dir
23 | if _, err := os.Stat("ffprobe"); os.IsNotExist(err) {
24 | ffprobe.SetFFProbeBinPath(filepath.Dir(os.Args[0]) + "/ffprobe")
25 | binFile = filepath.Dir(os.Args[0]) + "/ffprobe"
26 | }
27 | }
28 | }
29 |
30 | func Exists() bool {
31 | _, err := os.Stat(binFile)
32 | return !os.IsNotExist(err)
33 | }
34 |
35 | func ProbeUrl(link string) (*ffprobe.ProbeData, error) {
36 | data, err := ffprobe.ProbeURL(getCtx(), link)
37 | return data, err
38 | }
39 |
40 | func ProbeReader(reader io.Reader) (*ffprobe.ProbeData, error) {
41 | data, err := ffprobe.ProbeReader(getCtx(), reader)
42 | return data, err
43 | }
44 |
45 | func getCtx() context.Context {
46 | ctx, cancel := context.WithCancel(context.Background())
47 | go func() {
48 | time.Sleep(5 * time.Minute)
49 | cancel()
50 | }()
51 | return ctx
52 | }
53 |
--------------------------------------------------------------------------------
/server/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 | "strings"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | var (
15 | logPath = ""
16 | webLogPath = ""
17 | )
18 |
19 | var webLog *log.Logger
20 |
21 | var (
22 | logFile *os.File
23 | webLogFile *os.File
24 | )
25 |
26 | func Init(path, webpath string) {
27 | webLogPath = webpath
28 | logPath = path
29 |
30 | if webpath != "" {
31 | ff, err := os.OpenFile(webLogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
32 | if err != nil {
33 | TLogln("Error create web log file:", err)
34 | } else {
35 | webLogFile = ff
36 | webLog = log.New(ff, " ", log.LstdFlags)
37 | }
38 | }
39 |
40 | if path != "" {
41 | if fi, err := os.Lstat(path); err == nil {
42 | if fi.Size() >= 100*1024*1024 { // 100MB
43 | os.Remove(path)
44 | }
45 | }
46 | ff, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
47 | if err != nil {
48 | TLogln("Error create log file:", err)
49 | return
50 | }
51 | logFile = ff
52 | os.Stdout = ff
53 | os.Stderr = ff
54 | // var timeFmt string
55 | // var ok bool
56 | // timeFmt, ok = os.LookupEnv("GO_LOG_TIME_FMT")
57 | // if !ok {
58 | // timeFmt = "2006-01-02T15:04:05-0700"
59 | // }
60 | // log.SetFlags(log.Lmsgprefix)
61 | // log.SetPrefix(time.Now().Format(timeFmt) + " TSM ")
62 | log.SetFlags(log.LstdFlags | log.LUTC | log.Lmsgprefix)
63 | log.SetPrefix("UTC0 ")
64 | log.SetOutput(ff)
65 | }
66 | }
67 |
68 | func Close() {
69 | if logFile != nil {
70 | logFile.Close()
71 | }
72 | if webLogFile != nil {
73 | webLogFile.Close()
74 | }
75 | }
76 |
77 | func TLogln(v ...interface{}) {
78 | log.Println(v...)
79 | }
80 |
81 | func WebLogln(v ...interface{}) {
82 | if webLog != nil {
83 | webLog.Println(v...)
84 | }
85 | }
86 |
87 | func WebLogger() gin.HandlerFunc {
88 | return func(c *gin.Context) {
89 | if webLog == nil {
90 | c.Next()
91 | return
92 | }
93 | body := ""
94 | // save body if not form or file
95 | if !strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
96 | body, _ := io.ReadAll(c.Request.Body)
97 | c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
98 | } else {
99 | body = "body hidden, too large"
100 | }
101 | c.Next()
102 |
103 | statusCode := c.Writer.Status()
104 | clientIP := c.ClientIP()
105 | method := c.Request.Method
106 | path := c.Request.URL.Path
107 | raw := c.Request.URL.RawQuery
108 | if raw != "" {
109 | path = path + "?" + raw
110 | }
111 |
112 | logStr := fmt.Sprintf("%3d | %12s | %-7s %#v %v",
113 | statusCode,
114 | clientIP,
115 | method,
116 | path,
117 | string(body),
118 | )
119 | WebLogln(logStr)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/server/rutor/mem_test.go:
--------------------------------------------------------------------------------
1 | package rutor
2 |
3 | import (
4 | "compress/flate"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | "testing"
10 |
11 | "server/rutor/models"
12 | )
13 |
14 | func TestParseChannel(t *testing.T) {
15 | channel := make(chan *models.TorrentDetails, 0)
16 | var ftors []*models.TorrentDetails
17 | go func() {
18 | for torr := range channel {
19 | ftors = append(ftors, torr)
20 | }
21 | }()
22 |
23 | path, _ := os.Getwd()
24 | ff, err := os.Open(filepath.Join(path, "rutor.ls"))
25 | if err == nil {
26 | defer ff.Close()
27 | r := flate.NewReader(ff)
28 | defer r.Close()
29 | dec := json.NewDecoder(r)
30 |
31 | _, err := dec.Token()
32 | if err != nil {
33 | t.Error(err)
34 | }
35 |
36 | for dec.More() {
37 | var torr *models.TorrentDetails
38 | err = dec.Decode(&torr)
39 | if err != nil {
40 | t.Error(err)
41 | }
42 | channel <- torr
43 | }
44 | close(channel)
45 | } else {
46 | t.Error(err)
47 | }
48 | }
49 |
50 | func TestParseArr(t *testing.T) {
51 | var ftors []*models.TorrentDetails
52 | path, _ := os.Getwd()
53 | ff, err := os.Open(filepath.Join(path, "rutor.ls"))
54 | if err == nil {
55 | defer ff.Close()
56 | r := flate.NewReader(ff)
57 | defer r.Close()
58 | dec := json.NewDecoder(r)
59 |
60 | _, err := dec.Token()
61 | if err != nil {
62 | t.Error(err)
63 | }
64 |
65 | for dec.More() {
66 | var torr *models.TorrentDetails
67 | err = dec.Decode(&torr)
68 | if err != nil {
69 | t.Error(err)
70 | }
71 | ftors = append(ftors, torr)
72 | fmt.Println(len(ftors))
73 | }
74 | } else {
75 | t.Error(err)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/server/rutor/models/torrentDetails.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "strings"
5 | "time"
6 | )
7 |
8 | const (
9 | CatMovie = "Movie"
10 | CatSeries = "Series"
11 | CatDocMovie = "DocMovie"
12 | CatDocSeries = "DocSeries"
13 | CatCartoonMovie = "CartoonMovie"
14 | CatCartoonSeries = "CartoonSeries"
15 | CatTVShow = "TVShow"
16 | CatAnime = "Anime"
17 |
18 | Q_LOWER = 0
19 | Q_WEBDL_720 = 100
20 | Q_BDRIP_720 = 101
21 | Q_BDRIP_HEVC_720 = 102
22 | Q_WEBDL_1080 = 200
23 | Q_BDRIP_1080 = 201
24 | Q_BDRIP_HEVC_1080 = 202
25 | Q_BDREMUX_1080 = 203
26 | Q_WEBDL_SDR_2160 = 300
27 | Q_WEBDL_HDR_2160 = 301
28 | Q_WEBDL_DV_2160 = 302
29 | Q_BDRIP_SDR_2160 = 303
30 | Q_BDRIP_HDR_2160 = 304
31 | Q_BDRIP_DV_2160 = 305
32 | Q_UHD_BDREMUX_SDR = 306
33 | Q_UHD_BDREMUX_HDR = 307
34 | Q_UHD_BDREMUX_DV = 308
35 |
36 | Q_UNKNOWN = 0
37 | Q_A = 1 // Авторский, по типу Гоблина или старых переводчиков
38 | Q_L1 = 100 // Любительский одноголосый закадровый
39 | Q_L2 = 101 // Любительский двухголосый закадровый
40 | Q_L = 102 // Любительский 3-5 человек закадровый
41 | Q_LS = 103 // Любительский студия
42 | Q_P1 = 200 // Професиональный одноголосый закадровый
43 | Q_P2 = 201 // Профессиональный двухголосый закадровый
44 | Q_P = 202 // Профессиональный 3-5 человек закадровый
45 | Q_PS = 203 // Профессиональный студия
46 | Q_D = 300 // Официальное профессиональное многоголосое озвучивание
47 | Q_LICENSE = 301 // Лицензия
48 | )
49 |
50 | type TorrentDetails struct {
51 | Title string
52 | Name string
53 | Names []string
54 | Categories string
55 | Size string
56 | CreateDate time.Time
57 | Tracker string
58 | Link string
59 | Year int
60 | Peer int
61 | Seed int
62 | Magnet string
63 | Hash string
64 | IMDBID string
65 | VideoQuality int
66 | AudioQuality int
67 | }
68 |
69 | type TorrentFile struct {
70 | Name string
71 | Size int64
72 | }
73 |
74 | func (d TorrentDetails) GetNames() string {
75 | return strings.Join(d.Names, " ")
76 | }
77 |
--------------------------------------------------------------------------------
/server/rutor/torrsearch/filter.go:
--------------------------------------------------------------------------------
1 | package torrsearch
2 |
3 | import (
4 | "strings"
5 |
6 | snowballeng "github.com/kljensen/snowball/english"
7 | snowballru "github.com/kljensen/snowball/russian"
8 | )
9 |
10 | // lowercaseFilter returns a slice of tokens normalized to lower case.
11 | func lowercaseFilter(tokens []string) []string {
12 | r := make([]string, len(tokens))
13 | for i, token := range tokens {
14 | r[i] = replaceChars(strings.ToLower(token))
15 | }
16 | return r
17 | }
18 |
19 | // stopwordFilter returns a slice of tokens with stop words removed.
20 | func stopwordFilter(tokens []string) []string {
21 | r := make([]string, 0, len(tokens))
22 | for _, token := range tokens {
23 | if !isStopWord(token) {
24 | r = append(r, token)
25 | }
26 | }
27 | return r
28 | }
29 |
30 | // stemmerFilter returns a slice of stemmed tokens.
31 | func stemmerFilter(tokens []string) []string {
32 | r := make([]string, len(tokens))
33 | for i, token := range tokens {
34 | worden := snowballeng.Stem(token, false)
35 | wordru := snowballru.Stem(token, false)
36 | if wordru == "" || worden == "" {
37 | continue
38 | }
39 | if wordru != token {
40 | r[i] = wordru
41 | } else {
42 | r[i] = worden
43 | }
44 | }
45 | return r
46 | }
47 |
48 | func replaceChars(word string) string {
49 | out := []rune(word)
50 | for i, r := range out {
51 | if r == 'ё' {
52 | out[i] = 'е'
53 | }
54 | }
55 | return string(out)
56 | }
57 |
58 | func isStopWord(word string) bool {
59 | switch word {
60 | case "a", "am", "an", "and", "are", "as", "at", "be",
61 | "by", "did", "do", "is", "of", "or", "s", "so", "t",
62 | "и", "в", "с", "со", "а", "но", "к", "у",
63 | "же", "бы", "по", "от", "о", "из", "ну",
64 | "ли", "ни", "нибудь", "уж", "ведь", "ж", "об":
65 | return true
66 | }
67 | return false
68 | }
69 |
--------------------------------------------------------------------------------
/server/rutor/torrsearch/index.go:
--------------------------------------------------------------------------------
1 | package torrsearch
2 |
3 | import (
4 | "server/rutor/models"
5 | )
6 |
7 | // Index is an inverted Index. It maps tokens to document IDs.
8 | type Index map[string][]int
9 |
10 | var idx Index
11 |
12 | func NewIndex(torrs []*models.TorrentDetails) {
13 | idx = make(Index)
14 | idx.add(torrs)
15 | }
16 |
17 | func Search(text string) []int {
18 | return idx.search(text)
19 | }
20 |
21 | func GetIDX() Index {
22 | return idx
23 | }
24 |
25 | func (idx Index) add(torrs []*models.TorrentDetails) {
26 | for ID, torr := range torrs {
27 | for _, token := range analyze(torr.Title) {
28 | ids := idx[token]
29 | if ids != nil && ids[len(ids)-1] == ID {
30 | // Don't add same ID twice.
31 | continue
32 | }
33 | idx[token] = append(ids, ID)
34 | }
35 | }
36 | }
37 |
38 | // intersection returns the set intersection between a and b.
39 | // a and b have to be sorted in ascending order and contain no duplicates.
40 | func intersection(a []int, b []int) []int {
41 | maxLen := len(a)
42 | if len(b) > maxLen {
43 | maxLen = len(b)
44 | }
45 | r := make([]int, 0, maxLen)
46 | var i, j int
47 | for i < len(a) && j < len(b) {
48 | if a[i] < b[j] {
49 | i++
50 | } else if a[i] > b[j] {
51 | j++
52 | } else {
53 | r = append(r, a[i])
54 | i++
55 | j++
56 | }
57 | }
58 | return r
59 | }
60 |
61 | // Search queries the Index for the given text.
62 | func (idx Index) search(text string) []int {
63 | var r []int
64 | for _, token := range analyze(text) {
65 | if ids, ok := idx[token]; ok {
66 | if r == nil {
67 | r = ids
68 | } else {
69 | r = intersection(r, ids)
70 | }
71 | } else {
72 | // Token doesn't exist.
73 | return nil
74 | }
75 | }
76 | return r
77 | }
78 |
--------------------------------------------------------------------------------
/server/rutor/torrsearch/tokenizer.go:
--------------------------------------------------------------------------------
1 | package torrsearch
2 |
3 | import (
4 | "strings"
5 | "unicode"
6 | )
7 |
8 | // tokenize returns a slice of tokens for the given text.
9 | func tokenize(text string) []string {
10 | return strings.FieldsFunc(text, func(r rune) bool {
11 | // Split on any character that is not a letter or a number.
12 | return !unicode.IsLetter(r) && !unicode.IsNumber(r)
13 | })
14 | }
15 |
16 | // analyze analyzes the text and returns a slice of tokens.
17 | func analyze(text string) []string {
18 | tokens := tokenize(text)
19 | tokens = lowercaseFilter(tokens)
20 | tokens = stopwordFilter(tokens)
21 | // tokens = stemmerFilter(tokens)
22 | return tokens
23 | }
24 |
--------------------------------------------------------------------------------
/server/rutor/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "os"
7 | "strings"
8 | )
9 |
10 | func ClearStr(str string) string {
11 | ret := ""
12 | str = strings.ToLower(str)
13 | for _, r := range str {
14 | if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'а' && r <= 'я') || r == 'ё' {
15 | ret = ret + string(r)
16 | }
17 | }
18 | return ret
19 | }
20 |
21 | func MD5File(fname string) string {
22 | f, err := os.Open(fname)
23 | if err != nil {
24 | return ""
25 | }
26 |
27 | defer f.Close()
28 |
29 | buf := make([]byte, 1024*1024)
30 | h := sha256.New()
31 |
32 | for {
33 | bytesRead, err := f.Read(buf)
34 | if err != nil {
35 | break
36 | }
37 |
38 | h.Write(buf[:bytesRead])
39 | }
40 |
41 | return hex.EncodeToString(h.Sum(nil))
42 | }
43 |
--------------------------------------------------------------------------------
/server/settings/dbreadcache.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "sync"
5 |
6 | "server/log"
7 | )
8 |
9 | type DBReadCache struct {
10 | db TorrServerDB
11 | listCache map[string][]string
12 | listCacheMutex sync.RWMutex
13 | dataCache map[[2]string][]byte
14 | dataCacheMutex sync.RWMutex
15 | }
16 |
17 | func NewDBReadCache(db TorrServerDB) TorrServerDB {
18 | cdb := &DBReadCache{
19 | db: db,
20 | listCache: map[string][]string{},
21 | dataCache: map[[2]string][]byte{},
22 | }
23 | return cdb
24 | }
25 |
26 | func (v *DBReadCache) CloseDB() {
27 | v.db.CloseDB()
28 | v.db = nil
29 | v.listCache = nil
30 | v.dataCache = nil
31 | }
32 |
33 | func (v *DBReadCache) Get(xPath, name string) []byte {
34 | cacheKey := v.makeDataCacheKey(xPath, name)
35 | v.dataCacheMutex.RLock()
36 | if data, ok := v.dataCache[cacheKey]; ok {
37 | defer v.dataCacheMutex.RUnlock()
38 | return data
39 | }
40 | v.dataCacheMutex.RUnlock()
41 | data := v.db.Get(xPath, name)
42 | v.dataCacheMutex.Lock()
43 | v.dataCache[cacheKey] = data
44 | v.dataCacheMutex.Unlock()
45 | return data
46 | }
47 |
48 | func (v *DBReadCache) Set(xPath, name string, value []byte) {
49 | if ReadOnly {
50 | log.TLogln("DB.Set: Read-only DB mode!", name)
51 | return
52 | }
53 | cacheKey := v.makeDataCacheKey(xPath, name)
54 | v.dataCacheMutex.Lock()
55 | v.dataCache[cacheKey] = value
56 | v.dataCacheMutex.Unlock()
57 | delete(v.listCache, xPath)
58 | v.db.Set(xPath, name, value)
59 | }
60 |
61 | func (v *DBReadCache) List(xPath string) []string {
62 | v.listCacheMutex.RLock()
63 | if names, ok := v.listCache[xPath]; ok {
64 | defer v.listCacheMutex.RUnlock()
65 | return names
66 | }
67 | v.listCacheMutex.RUnlock()
68 | names := v.db.List(xPath)
69 | v.listCacheMutex.Lock()
70 | v.listCache[xPath] = names
71 | v.listCacheMutex.Unlock()
72 | return names
73 | }
74 |
75 | func (v *DBReadCache) Rem(xPath, name string) {
76 | if ReadOnly {
77 | log.TLogln("DB.Rem: Read-only DB mode!", name)
78 | return
79 | }
80 | cacheKey := v.makeDataCacheKey(xPath, name)
81 | delete(v.dataCache, cacheKey)
82 | delete(v.listCache, xPath)
83 | v.db.Rem(xPath, name)
84 | }
85 |
86 | func (v *DBReadCache) makeDataCacheKey(xPath, name string) [2]string {
87 | return [2]string{xPath, name}
88 | }
89 |
--------------------------------------------------------------------------------
/server/settings/settings.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | "server/log"
8 | )
9 |
10 | var (
11 | tdb TorrServerDB
12 | Path string
13 | IP string
14 | Port string
15 | Ssl bool
16 | SslPort string
17 | ReadOnly bool
18 | HttpAuth bool
19 | SearchWA bool
20 | PubIPv4 string
21 | PubIPv6 string
22 | TorAddr string
23 | MaxSize int64
24 | )
25 |
26 | func InitSets(readOnly, searchWA bool) {
27 | ReadOnly = readOnly
28 | SearchWA = searchWA
29 |
30 | bboltDB := NewTDB()
31 | if bboltDB == nil {
32 | log.TLogln("Error open bboltDB:", filepath.Join(Path, "config.db"))
33 | os.Exit(1)
34 | }
35 |
36 | jsonDB := NewJsonDB()
37 | if jsonDB == nil {
38 | log.TLogln("Error open jsonDB")
39 | os.Exit(1)
40 | }
41 |
42 | dbRouter := NewXPathDBRouter()
43 | // First registered DB becomes default route
44 | dbRouter.RegisterRoute(jsonDB, "Settings")
45 | dbRouter.RegisterRoute(jsonDB, "Viewed")
46 | dbRouter.RegisterRoute(bboltDB, "Torrents")
47 |
48 | tdb = NewDBReadCache(dbRouter)
49 |
50 | // We migrate settings here, it must be done before loadBTSets()
51 | if err := MigrateToJson(bboltDB, jsonDB); err != nil {
52 | log.TLogln("MigrateToJson failed")
53 | os.Exit(1)
54 | }
55 | loadBTSets()
56 | MigrateTorrents()
57 | }
58 |
59 | func CloseDB() {
60 | tdb.CloseDB()
61 | }
62 |
--------------------------------------------------------------------------------
/server/settings/torrent.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "encoding/json"
5 | "sort"
6 | "sync"
7 |
8 | "github.com/anacrolix/torrent"
9 | "github.com/anacrolix/torrent/metainfo"
10 | )
11 |
12 | type TorrentDB struct {
13 | *torrent.TorrentSpec
14 |
15 | Title string `json:"title,omitempty"`
16 | Category string `json:"category,omitempty"`
17 | Poster string `json:"poster,omitempty"`
18 | Data string `json:"data,omitempty"`
19 |
20 | Timestamp int64 `json:"timestamp,omitempty"`
21 | Size int64 `json:"size,omitempty"`
22 | }
23 |
24 | type File struct {
25 | Name string `json:"name,omitempty"`
26 | Id int `json:"id,omitempty"`
27 | Size int64 `json:"size,omitempty"`
28 | }
29 |
30 | var mu sync.Mutex
31 |
32 | func AddTorrent(torr *TorrentDB) {
33 | list := ListTorrent()
34 | mu.Lock()
35 | find := -1
36 | for i, db := range list {
37 | if db.InfoHash.HexString() == torr.InfoHash.HexString() {
38 | find = i
39 | break
40 | }
41 | }
42 | if find != -1 {
43 | list[find] = torr
44 | } else {
45 | list = append(list, torr)
46 | }
47 | for _, db := range list {
48 | buf, err := json.Marshal(db)
49 | if err == nil {
50 | tdb.Set("Torrents", db.InfoHash.HexString(), buf)
51 | }
52 | }
53 | mu.Unlock()
54 | }
55 |
56 | func ListTorrent() []*TorrentDB {
57 | mu.Lock()
58 | defer mu.Unlock()
59 |
60 | var list []*TorrentDB
61 | keys := tdb.List("Torrents")
62 | for _, key := range keys {
63 | buf := tdb.Get("Torrents", key)
64 | if len(buf) > 0 {
65 | var torr *TorrentDB
66 | err := json.Unmarshal(buf, &torr)
67 | if err == nil {
68 | list = append(list, torr)
69 | }
70 | }
71 | }
72 | sort.Slice(list, func(i, j int) bool {
73 | return list[i].Timestamp > list[j].Timestamp
74 | })
75 | return list
76 | }
77 |
78 | func RemTorrent(hash metainfo.Hash) {
79 | mu.Lock()
80 | tdb.Rem("Torrents", hash.HexString())
81 | mu.Unlock()
82 | }
83 |
--------------------------------------------------------------------------------
/server/settings/torrserverdb.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | type TorrServerDB interface {
4 | CloseDB()
5 | Get(xPath, name string) []byte
6 | Set(xPath, name string, value []byte)
7 | List(xPath string) []string
8 | Rem(xPath, name string)
9 | }
10 |
--------------------------------------------------------------------------------
/server/settings/viewed.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "server/log"
7 | )
8 |
9 | type Viewed struct {
10 | Hash string `json:"hash"`
11 | FileIndex int `json:"file_index"`
12 | }
13 |
14 | func SetViewed(vv *Viewed) {
15 | var indexes map[int]struct{}
16 | var err error
17 |
18 | buf := tdb.Get("Viewed", vv.Hash)
19 | if len(buf) == 0 {
20 | indexes = make(map[int]struct{})
21 | indexes[vv.FileIndex] = struct{}{}
22 | buf, err = json.Marshal(indexes)
23 | if err == nil {
24 | tdb.Set("Viewed", vv.Hash, buf)
25 | }
26 | } else {
27 | err = json.Unmarshal(buf, &indexes)
28 | if err == nil {
29 | indexes[vv.FileIndex] = struct{}{}
30 | buf, err = json.Marshal(indexes)
31 | if err == nil {
32 | tdb.Set("Viewed", vv.Hash, buf)
33 | }
34 | }
35 | }
36 | if err != nil {
37 | log.TLogln("Error set viewed:", err)
38 | }
39 | }
40 |
41 | func RemViewed(vv *Viewed) {
42 | buf := tdb.Get("Viewed", vv.Hash)
43 | var indeces map[int]struct{}
44 | err := json.Unmarshal(buf, &indeces)
45 | if err == nil {
46 | if vv.FileIndex != -1 {
47 | delete(indeces, vv.FileIndex)
48 | buf, err = json.Marshal(indeces)
49 | if err == nil {
50 | tdb.Set("Viewed", vv.Hash, buf)
51 | }
52 | } else {
53 | tdb.Rem("Viewed", vv.Hash)
54 | }
55 | }
56 | if err != nil {
57 | log.TLogln("Error rem viewed:", err)
58 | }
59 | }
60 |
61 | func ListViewed(hash string) []*Viewed {
62 | var err error
63 | if hash != "" {
64 | buf := tdb.Get("Viewed", hash)
65 | if len(buf) == 0 {
66 | return []*Viewed{}
67 | }
68 | var indeces map[int]struct{}
69 | err = json.Unmarshal(buf, &indeces)
70 | if err == nil {
71 | var ret []*Viewed
72 | for i := range indeces {
73 | ret = append(ret, &Viewed{hash, i})
74 | }
75 | return ret
76 | }
77 | } else {
78 | var ret []*Viewed
79 | keys := tdb.List("Viewed")
80 | for _, key := range keys {
81 | buf := tdb.Get("Viewed", key)
82 | if len(buf) == 0 {
83 | return []*Viewed{}
84 | }
85 | var indeces map[int]struct{}
86 | err = json.Unmarshal(buf, &indeces)
87 | if err == nil {
88 | for i := range indeces {
89 | ret = append(ret, &Viewed{key, i})
90 | }
91 | }
92 | }
93 | return ret
94 | }
95 |
96 | log.TLogln("Error list viewed:", err)
97 | return []*Viewed{}
98 | }
99 |
--------------------------------------------------------------------------------
/server/settings/xpathdbrouter.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "sort"
7 | "strings"
8 |
9 | "server/log"
10 |
11 | "golang.org/x/exp/slices"
12 | )
13 |
14 | type XPathDBRouter struct {
15 | dbs []TorrServerDB
16 | routes []string
17 | route2db map[string]TorrServerDB
18 | dbNames map[TorrServerDB]string
19 | }
20 |
21 | func NewXPathDBRouter() *XPathDBRouter {
22 | router := &XPathDBRouter{
23 | dbs: []TorrServerDB{},
24 | dbNames: map[TorrServerDB]string{},
25 | routes: []string{},
26 | route2db: map[string]TorrServerDB{},
27 | }
28 | return router
29 | }
30 |
31 | func (v *XPathDBRouter) RegisterRoute(db TorrServerDB, xPath string) error {
32 | newRoute := v.xPathToRoute(xPath)
33 |
34 | if slices.Contains(v.routes, newRoute) {
35 | return fmt.Errorf("route \"%s\" already in routing table", newRoute)
36 | }
37 |
38 | // First DB becomes Default DB with default route
39 | if len(v.dbs) == 0 && len(newRoute) != 0 {
40 | v.RegisterRoute(db, "")
41 | }
42 |
43 | if !slices.Contains(v.dbs, db) {
44 | v.dbs = append(v.dbs, db)
45 | v.dbNames[db] = reflect.TypeOf(db).Elem().Name()
46 | v.log(fmt.Sprintf("Registered new DB \"%s\", total %d DBs registered", v.getDBName(db), len(v.dbs)))
47 | }
48 |
49 | v.route2db[newRoute] = db
50 | v.routes = append(v.routes, newRoute)
51 |
52 | // Sort routes by length descending.
53 | // It is important later to help selecting
54 | // most suitable route in getDBForXPath(xPath)
55 | sort.Slice(v.routes, func(iLeft, iRight int) bool {
56 | return len(v.routes[iLeft]) > len(v.routes[iRight])
57 | })
58 | v.log(fmt.Sprintf("Registered new route \"%s\" for DB \"%s\", total %d routes", newRoute, v.getDBName(db), len(v.routes)))
59 | return nil
60 | }
61 |
62 | func (v *XPathDBRouter) xPathToRoute(xPath string) string {
63 | return strings.ToLower(strings.TrimSpace(xPath))
64 | }
65 |
66 | func (v *XPathDBRouter) getDBForXPath(xPath string) TorrServerDB {
67 | if len(v.dbs) == 0 {
68 | return nil
69 | }
70 | lookup_route := v.xPathToRoute(xPath)
71 | var db TorrServerDB = nil
72 | // Expected v.routes sorted by length descending
73 | for _, route_prefix := range v.routes {
74 | if strings.HasPrefix(lookup_route, route_prefix) {
75 | db = v.route2db[route_prefix]
76 | break
77 | }
78 | }
79 | return db
80 | }
81 |
82 | func (v *XPathDBRouter) Get(xPath, name string) []byte {
83 | return v.getDBForXPath(xPath).Get(xPath, name)
84 | }
85 |
86 | func (v *XPathDBRouter) Set(xPath, name string, value []byte) {
87 | v.getDBForXPath(xPath).Set(xPath, name, value)
88 | }
89 |
90 | func (v *XPathDBRouter) List(xPath string) []string {
91 | return v.getDBForXPath(xPath).List(xPath)
92 | }
93 |
94 | func (v *XPathDBRouter) Rem(xPath, name string) {
95 | v.getDBForXPath(xPath).Rem(xPath, name)
96 | }
97 |
98 | func (v *XPathDBRouter) CloseDB() {
99 | for _, db := range v.dbs {
100 | db.CloseDB()
101 | }
102 | v.dbs = nil
103 | v.routes = nil
104 | v.route2db = nil
105 | v.dbNames = nil
106 | }
107 |
108 | func (v *XPathDBRouter) getDBName(db TorrServerDB) string {
109 | return v.dbNames[db]
110 | }
111 |
112 | func (v *XPathDBRouter) log(s string, params ...interface{}) {
113 | if len(params) > 0 {
114 | log.TLogln(fmt.Sprintf("XPathDBRouter: %s: %s", s, fmt.Sprint(params...)))
115 | } else {
116 | log.TLogln(fmt.Sprintf("XPathDBRouter: %s", s))
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/server/tgbot/add.go:
--------------------------------------------------------------------------------
1 | package tgbot
2 |
3 | import (
4 | "errors"
5 | tele "gopkg.in/telebot.v4"
6 | "server/log"
7 | set "server/settings"
8 | "server/torr"
9 | "server/web/api/utils"
10 | "strings"
11 | )
12 |
13 | func addTorrent(c tele.Context, link string) error {
14 | msg, err := c.Bot().Send(c.Sender(), "Подключение к торренту...")
15 | if err != nil {
16 | return err
17 | }
18 | log.TLogln("tg add torrent", link)
19 | link = strings.ReplaceAll(link, "&", "&")
20 | torrSpec, err := utils.ParseLink(link)
21 |
22 | if err != nil {
23 | log.TLogln("tg error parse link:", err)
24 | return err
25 | }
26 |
27 | tor, err := torr.AddTorrent(torrSpec, "", "", "", "")
28 |
29 | if tor.Data != "" && set.BTsets.EnableDebug {
30 | log.TLogln("torrent data:", tor.Data)
31 | }
32 | if tor.Category != "" && set.BTsets.EnableDebug {
33 | log.TLogln("torrent category:", tor.Category)
34 | }
35 |
36 | if err != nil {
37 | log.TLogln("tg error add torrent:", err)
38 | c.Bot().Edit(msg, "Ошибка при подключении: "+err.Error())
39 | return err
40 | }
41 |
42 | if !tor.GotInfo() {
43 | log.TLogln("tg error add torrent: timeout connection get torrent info")
44 | c.Bot().Edit(msg, "Ошибка при добаваления торрента: timeout connection get torrent info")
45 | return errors.New("timeout connection get torrent info")
46 | }
47 |
48 | if tor.Title == "" {
49 | tor.Title = torrSpec.DisplayName // prefer dn over name
50 | tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "")
51 | tor.Title = strings.ReplaceAll(tor.Title, "_", " ")
52 | tor.Title = strings.Trim(tor.Title, " ")
53 | if tor.Title == "" {
54 | tor.Title = tor.Name()
55 | }
56 | }
57 |
58 | torr.SaveTorrentToDB(tor)
59 |
60 | c.Bot().Edit(msg, "Торрент добавлен:\n"+link+"
")
61 |
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/server/tgbot/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "path/filepath"
7 | "server/log"
8 | "server/settings"
9 | )
10 |
11 | type Config struct {
12 | HostTG string
13 | HostWeb string
14 | WhiteIds []int64
15 | BlackIds []int64
16 | }
17 |
18 | var Cfg *Config
19 |
20 | func LoadConfig() {
21 | Cfg = &Config{}
22 | fn := filepath.Join(settings.Path, "tg.cfg")
23 | buf, err := os.ReadFile(fn)
24 | if err != nil {
25 | Cfg.WhiteIds = []int64{}
26 | Cfg.BlackIds = []int64{}
27 | Cfg.HostTG = "https://api.telegram.org"
28 | buf, _ = json.MarshalIndent(Cfg, "", " ")
29 | if buf != nil {
30 | os.WriteFile(fn, buf, 0666)
31 | }
32 | return
33 | }
34 | err = json.Unmarshal(buf, &Cfg)
35 | if err != nil {
36 | log.TLogln("Error read tg config:", err)
37 | }
38 | if Cfg.HostTG == "" {
39 | Cfg.HostTG = "https://api.telegram.org"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/tgbot/delete.go:
--------------------------------------------------------------------------------
1 | package tgbot
2 |
3 | import (
4 | tele "gopkg.in/telebot.v4"
5 | "server/torr"
6 | )
7 |
8 | func deleteTorrent(c tele.Context) {
9 | args := c.Args()
10 | hash := args[1]
11 | torr.RemTorrent(hash)
12 | return
13 | }
14 |
15 | func clear(c tele.Context) error {
16 | torrents := torr.ListTorrent()
17 | for _, t := range torrents {
18 | torr.RemTorrent(t.TorrentSpec.InfoHash.HexString())
19 | }
20 | return nil
21 | }
22 |
--------------------------------------------------------------------------------
/server/tgbot/files.go:
--------------------------------------------------------------------------------
1 | package tgbot
2 |
3 | import (
4 | "github.com/dustin/go-humanize"
5 | tele "gopkg.in/telebot.v4"
6 | "path/filepath"
7 | "server/log"
8 | "server/settings"
9 | "server/tgbot/config"
10 | "server/torr"
11 | "server/web"
12 | "strconv"
13 | "strings"
14 | "time"
15 | )
16 |
17 | func files(c tele.Context) error {
18 | args := c.Args()
19 | msg, err := c.Bot().Send(c.Sender(), "Подключение к торренту...")
20 | t := torr.GetTorrent(args[1])
21 | if t == nil {
22 | c.Edit(msg, "Torrent not connected: "+args[1])
23 | return nil
24 | }
25 | if err == nil {
26 | go func() {
27 | for !t.WaitInfo() {
28 | time.Sleep(time.Second)
29 | t = torr.GetTorrent(args[1])
30 | }
31 | c.Bot().Delete(msg)
32 | host := config.Cfg.HostWeb
33 | if host == "" {
34 | host = settings.PubIPv4
35 | if host == "" {
36 | ips := web.GetLocalIps()
37 | if len(ips) == 0 {
38 | host = "127.0.0.1"
39 | } else {
40 | host = ips[0]
41 | }
42 | }
43 | }
44 | if !strings.Contains(host, ":") {
45 | host += ":" + settings.Port
46 | }
47 | if !strings.HasPrefix(host, "http") {
48 | host = "http://" + host
49 | }
50 |
51 | t = torr.GetTorrent(args[1])
52 | ti := t.Status()
53 |
54 | txt := "" + ti.Title + " " +
55 | "" + humanize.Bytes(uint64(ti.TorrentSize)) + "\n\n" +
56 | "" + ti.Hash + "
"
57 |
58 | filesKbd := &tele.ReplyMarkup{}
59 | var files []tele.Row
60 |
61 | i := len(txt)
62 | for _, f := range ti.FileStats {
63 | btn := filesKbd.Data("#"+strconv.Itoa(f.Id)+": "+humanize.Bytes(uint64(f.Length))+"\n"+filepath.Base(f.Path), "upload", ti.Hash, strconv.Itoa(f.Id))
64 | link := filesKbd.URL("Ссылка", host+"/stream/"+filepath.Base(f.Path)+"?link="+t.Hash().HexString()+"&index="+strconv.Itoa(f.Id)+"&play")
65 | files = append(files, filesKbd.Row(btn, link))
66 | if i+len(txt) > 1024 || len(files) > 99 {
67 | filesKbd := &tele.ReplyMarkup{}
68 | filesKbd.Inline(files...)
69 | err = c.Send(txt, filesKbd)
70 | if err != nil {
71 | log.TLogln("Error send message files:", err)
72 | return
73 | }
74 | files = files[:0]
75 | i = len(txt)
76 | }
77 | i += len(btn.Text + link.Text)
78 | }
79 |
80 | if len(files) > 0 {
81 | filesKbd.Inline(files...)
82 | err = c.Send(txt, filesKbd)
83 | if err != nil {
84 | log.TLogln("Error send message files:", err)
85 | return
86 | }
87 | }
88 |
89 | if len(files) > 1 {
90 | txt = "" + ti.Title + " " +
91 | "" + humanize.Bytes(uint64(ti.TorrentSize)) + "\n\n" +
92 | "" + ti.Hash + "
\n\n" +
93 | "Чтобы скачать несколько файлов, ответьте на это сообщение, с какого файла скачать по какой, пример: 2-12\n\n" +
94 | "Скачать все файлы? Всего:" + strconv.Itoa(len(ti.FileStats))
95 | files = files[:0]
96 | files = append(files, filesKbd.Row(filesKbd.Data("Скачать все файлы", "uploadall", ti.Hash)))
97 | filesKbd.Inline(files...)
98 | err = c.Send(txt, filesKbd)
99 | if err != nil {
100 | log.TLogln("Error send message files:", err)
101 | return
102 | }
103 | }
104 | }()
105 | }
106 | return err
107 | }
108 |
--------------------------------------------------------------------------------
/server/tgbot/list.go:
--------------------------------------------------------------------------------
1 | package tgbot
2 |
3 | import (
4 | "github.com/dustin/go-humanize"
5 | tele "gopkg.in/telebot.v4"
6 | "server/torr"
7 | )
8 |
9 | func list(c tele.Context) error {
10 | torrents := torr.ListTorrent()
11 |
12 | for _, t := range torrents {
13 | btnFiles := tele.InlineButton{Text: "Файлы", Unique: "files", Data: t.Hash().String()}
14 | btnDelete := tele.InlineButton{Text: "Удалить", Unique: "delete", Data: t.Hash().String()}
15 | torrKbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnFiles, btnDelete}}}
16 | if t.Size > 0 {
17 | c.Send(""+t.Title+" "+humanize.Bytes(uint64(t.Size))+"", torrKbd)
18 | } else {
19 | c.Send(""+t.Title+"", torrKbd)
20 | }
21 | }
22 |
23 | if len(torrents) == 0 {
24 | c.Send("Нет торрентов")
25 | }
26 |
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/server/tgbot/upload.go:
--------------------------------------------------------------------------------
1 | package tgbot
2 |
3 | import (
4 | tele "gopkg.in/telebot.v4"
5 | up "server/tgbot/upload"
6 | "strconv"
7 | )
8 |
9 | func upload(c tele.Context) error {
10 | args := c.Args()
11 | idstr := args[2]
12 | id, err := strconv.Atoi(idstr)
13 | if err != nil {
14 | return err
15 | }
16 | up.AddRange(c, args[1], id, id)
17 | return nil
18 | }
19 |
20 | func uploadall(c tele.Context) {
21 | args := c.Args()
22 | up.AddRange(c, args[1], 1, -1)
23 | }
24 |
--------------------------------------------------------------------------------
/server/tgbot/upload/torrfile.go:
--------------------------------------------------------------------------------
1 | package upload
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/anacrolix/torrent"
7 | "log"
8 | "path/filepath"
9 | sets "server/settings"
10 | "server/tgbot/config"
11 | "server/torr"
12 | "server/torr/state"
13 | "server/torr/storage/torrstor"
14 | )
15 |
16 | var ERR_STOPPED = errors.New("stopped")
17 |
18 | type TorrFile struct {
19 | hash string
20 | name string
21 | wrk *Worker
22 | offset int64
23 | size int64
24 | id int
25 |
26 | reader *torrstor.Reader
27 | }
28 |
29 | func NewTorrFile(wrk *Worker, stFile *state.TorrentFileStat) (*TorrFile, error) {
30 | if config.Cfg.HostTG != "" && stFile.Length > 2*1024*1024*1024 {
31 | return nil, errors.New("Размер файла должен быть больше 2GB")
32 | }
33 | if config.Cfg.HostTG == "" && stFile.Length > 50*1024*1024 {
34 | return nil, errors.New("Размер файла должен быть больше 50MB\nЧтобы закачивать файлы до 2GB нужно в tg.cfg указать host к telegram bot api")
35 | }
36 |
37 | tf := new(TorrFile)
38 | tf.hash = wrk.torrentHash
39 | tf.name = filepath.Base(stFile.Path)
40 | tf.wrk = wrk
41 | tf.size = stFile.Length
42 |
43 | t := torr.GetTorrent(wrk.torrentHash)
44 | t.WaitInfo()
45 |
46 | files := t.Files()
47 | var file *torrent.File
48 | for _, tfile := range files {
49 | if tfile.Path() == stFile.Path {
50 | file = tfile
51 | break
52 | }
53 | }
54 | if file == nil {
55 | return nil, fmt.Errorf("file with id %v not found", stFile.Id)
56 | }
57 | if int64(sets.MaxSize) > 0 && file.Length() > int64(sets.MaxSize) {
58 | log.Println("file", file.DisplayPath(), "size exceeded max allowed", sets.MaxSize, "bytes")
59 | return nil, fmt.Errorf("file size exceeded max allowed %d bytes", sets.MaxSize)
60 | }
61 |
62 | reader := t.NewReader(file)
63 | if sets.BTsets.ResponsiveMode {
64 | reader.SetResponsive()
65 | }
66 | tf.reader = reader
67 |
68 | return tf, nil
69 | }
70 |
71 | func (t *TorrFile) Read(p []byte) (n int, err error) {
72 | if t.wrk.isCancelled {
73 | return 0, ERR_STOPPED
74 | }
75 | n, err = t.reader.Read(p)
76 | t.offset += int64(n)
77 | return
78 | }
79 |
80 | func (t *TorrFile) Loaded() int64 {
81 | return t.size - t.offset
82 | }
83 |
84 | func (t *TorrFile) Close() {
85 | if t.reader != nil {
86 | t.reader.Close()
87 | t.reader = nil
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/server/torr/dbwrapper.go:
--------------------------------------------------------------------------------
1 | package torr
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "server/settings"
7 | "server/torr/state"
8 | "server/torr/utils"
9 |
10 | "github.com/anacrolix/torrent/metainfo"
11 | )
12 |
13 | type tsFiles struct {
14 | TorrServer struct {
15 | Files []*state.TorrentFileStat `json:"Files"`
16 | } `json:"TorrServer"`
17 | }
18 |
19 | func AddTorrentDB(torr *Torrent) {
20 | t := new(settings.TorrentDB)
21 | t.TorrentSpec = torr.TorrentSpec
22 | t.Title = torr.Title
23 | t.Category = torr.Category
24 | if torr.Data == "" {
25 | files := new(tsFiles)
26 | files.TorrServer.Files = torr.Status().FileStats
27 | buf, err := json.Marshal(files)
28 | if err == nil {
29 | t.Data = string(buf)
30 | torr.Data = t.Data
31 | }
32 | } else {
33 | t.Data = torr.Data
34 | }
35 | if utils.CheckImgUrl(torr.Poster) {
36 | t.Poster = torr.Poster
37 | }
38 | t.Size = torr.Size
39 | if t.Size == 0 && torr.Torrent != nil {
40 | t.Size = torr.Torrent.Length()
41 | }
42 | // don't override timestamp from DB on edit
43 | t.Timestamp = torr.Timestamp // time.Now().Unix()
44 |
45 | settings.AddTorrent(t)
46 | }
47 |
48 | func GetTorrentDB(hash metainfo.Hash) *Torrent {
49 | list := settings.ListTorrent()
50 | for _, db := range list {
51 | if hash == db.InfoHash {
52 | torr := new(Torrent)
53 | torr.TorrentSpec = db.TorrentSpec
54 | torr.Title = db.Title
55 | torr.Poster = db.Poster
56 | torr.Category = db.Category
57 | torr.Timestamp = db.Timestamp
58 | torr.Size = db.Size
59 | torr.Data = db.Data
60 | torr.Stat = state.TorrentInDB
61 | return torr
62 | }
63 | }
64 | return nil
65 | }
66 |
67 | func RemTorrentDB(hash metainfo.Hash) {
68 | settings.RemTorrent(hash)
69 | }
70 |
71 | func ListTorrentsDB() map[metainfo.Hash]*Torrent {
72 | ret := make(map[metainfo.Hash]*Torrent)
73 | list := settings.ListTorrent()
74 | for _, db := range list {
75 | torr := new(Torrent)
76 | torr.TorrentSpec = db.TorrentSpec
77 | torr.Title = db.Title
78 | torr.Poster = db.Poster
79 | torr.Category = db.Category
80 | torr.Timestamp = db.Timestamp
81 | torr.Size = db.Size
82 | torr.Data = db.Data
83 | torr.Stat = state.TorrentInDB
84 | ret[torr.TorrentSpec.InfoHash] = torr
85 | }
86 | return ret
87 | }
88 |
--------------------------------------------------------------------------------
/server/torr/state/state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | type TorrentStat int
4 |
5 | func (t TorrentStat) String() string {
6 | switch t {
7 | case TorrentAdded:
8 | return "Torrent added"
9 | case TorrentGettingInfo:
10 | return "Torrent getting info"
11 | case TorrentPreload:
12 | return "Torrent preload"
13 | case TorrentWorking:
14 | return "Torrent working"
15 | case TorrentClosed:
16 | return "Torrent closed"
17 | case TorrentInDB:
18 | return "Torrent in db"
19 | default:
20 | return "Torrent unknown status"
21 | }
22 | }
23 |
24 | const (
25 | TorrentAdded = TorrentStat(iota)
26 | TorrentGettingInfo
27 | TorrentPreload
28 | TorrentWorking
29 | TorrentClosed
30 | TorrentInDB
31 | )
32 |
33 | type TorrentStatus struct {
34 | Title string `json:"title"`
35 | Category string `json:"category"`
36 | Poster string `json:"poster"`
37 | Data string `json:"data,omitempty"`
38 | Timestamp int64 `json:"timestamp"`
39 | Name string `json:"name,omitempty"`
40 | Hash string `json:"hash,omitempty"`
41 | Stat TorrentStat `json:"stat"`
42 | StatString string `json:"stat_string"`
43 | LoadedSize int64 `json:"loaded_size,omitempty"`
44 | TorrentSize int64 `json:"torrent_size,omitempty"`
45 | PreloadedBytes int64 `json:"preloaded_bytes,omitempty"`
46 | PreloadSize int64 `json:"preload_size,omitempty"`
47 | DownloadSpeed float64 `json:"download_speed,omitempty"`
48 | UploadSpeed float64 `json:"upload_speed,omitempty"`
49 | TotalPeers int `json:"total_peers,omitempty"`
50 | PendingPeers int `json:"pending_peers,omitempty"`
51 | ActivePeers int `json:"active_peers,omitempty"`
52 | ConnectedSeeders int `json:"connected_seeders,omitempty"`
53 | HalfOpenPeers int `json:"half_open_peers,omitempty"`
54 | BytesWritten int64 `json:"bytes_written,omitempty"`
55 | BytesWrittenData int64 `json:"bytes_written_data,omitempty"`
56 | BytesRead int64 `json:"bytes_read,omitempty"`
57 | BytesReadData int64 `json:"bytes_read_data,omitempty"`
58 | BytesReadUsefulData int64 `json:"bytes_read_useful_data,omitempty"`
59 | ChunksWritten int64 `json:"chunks_written,omitempty"`
60 | ChunksRead int64 `json:"chunks_read,omitempty"`
61 | ChunksReadUseful int64 `json:"chunks_read_useful,omitempty"`
62 | ChunksReadWasted int64 `json:"chunks_read_wasted,omitempty"`
63 | PiecesDirtiedGood int64 `json:"pieces_dirtied_good,omitempty"`
64 | PiecesDirtiedBad int64 `json:"pieces_dirtied_bad,omitempty"`
65 | DurationSeconds float64 `json:"duration_seconds,omitempty"`
66 | BitRate string `json:"bit_rate,omitempty"`
67 |
68 | FileStats []*TorrentFileStat `json:"file_stats,omitempty"`
69 | }
70 |
71 | type TorrentFileStat struct {
72 | Id int `json:"id,omitempty"`
73 | Path string `json:"path,omitempty"`
74 | Length int64 `json:"length,omitempty"`
75 | }
76 |
--------------------------------------------------------------------------------
/server/torr/storage/state/state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "server/torr/state"
5 | )
6 |
7 | type CacheState struct {
8 | Hash string
9 | Capacity int64
10 | Filled int64
11 | PiecesLength int64
12 | PiecesCount int
13 | Torrent *state.TorrentStatus
14 | Pieces map[int]ItemState
15 | Readers []*ReaderState
16 | }
17 |
18 | type ItemState struct {
19 | Id int
20 | Length int64
21 | Size int64
22 | Completed bool
23 | Priority int
24 | }
25 |
26 | type ReaderState struct {
27 | Start int
28 | End int
29 | Reader int
30 | }
31 |
--------------------------------------------------------------------------------
/server/torr/storage/storage.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "github.com/anacrolix/torrent/metainfo"
5 | "github.com/anacrolix/torrent/storage"
6 | )
7 |
8 | type Storage interface {
9 | storage.ClientImpl
10 |
11 | CloseHash(hash metainfo.Hash)
12 | }
13 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/diskpiece.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path/filepath"
7 | "strconv"
8 | "sync"
9 | "time"
10 |
11 | "server/log"
12 | "server/settings"
13 | )
14 |
15 | type DiskPiece struct {
16 | piece *Piece
17 |
18 | name string
19 |
20 | mu sync.RWMutex
21 | }
22 |
23 | func NewDiskPiece(p *Piece) *DiskPiece {
24 | name := filepath.Join(settings.BTsets.TorrentsSavePath, p.cache.hash.HexString(), strconv.Itoa(p.Id))
25 | ff, err := os.Stat(name)
26 | if err == nil {
27 | p.Size = ff.Size()
28 | p.Complete = ff.Size() == p.cache.pieceLength
29 | p.Accessed = ff.ModTime().Unix()
30 | }
31 | return &DiskPiece{piece: p, name: name}
32 | }
33 |
34 | func (p *DiskPiece) WriteAt(b []byte, off int64) (n int, err error) {
35 | p.mu.Lock()
36 | defer p.mu.Unlock()
37 |
38 | ff, err := os.OpenFile(p.name, os.O_RDWR|os.O_CREATE, 0o666)
39 | if err != nil {
40 | log.TLogln("Error open file:", err)
41 | return 0, err
42 | }
43 | defer ff.Close()
44 | n, err = ff.WriteAt(b, off)
45 |
46 | p.piece.Size += int64(n)
47 | if p.piece.Size > p.piece.cache.pieceLength {
48 | p.piece.Size = p.piece.cache.pieceLength
49 | }
50 | p.piece.Accessed = time.Now().Unix()
51 | return
52 | }
53 |
54 | func (p *DiskPiece) ReadAt(b []byte, off int64) (n int, err error) {
55 | p.mu.Lock()
56 | defer p.mu.Unlock()
57 |
58 | ff, err := os.OpenFile(p.name, os.O_RDONLY, 0o666)
59 | if os.IsNotExist(err) {
60 | return 0, io.EOF
61 | }
62 | if err != nil {
63 | log.TLogln("Error open file:", err)
64 | return 0, err
65 | }
66 | defer ff.Close()
67 |
68 | n, err = ff.ReadAt(b, off)
69 |
70 | p.piece.Accessed = time.Now().Unix()
71 | if int64(len(b))+off >= p.piece.Size {
72 | go p.piece.cache.cleanPieces()
73 | }
74 | return n, nil
75 | }
76 |
77 | func (p *DiskPiece) Release() {
78 | p.mu.Lock()
79 | defer p.mu.Unlock()
80 |
81 | p.piece.Size = 0
82 | p.piece.Complete = false
83 |
84 | os.Remove(p.name)
85 | }
86 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/mempiece.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "io"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type MemPiece struct {
10 | piece *Piece
11 |
12 | buffer []byte
13 | mu sync.RWMutex
14 | }
15 |
16 | func NewMemPiece(p *Piece) *MemPiece {
17 | return &MemPiece{piece: p}
18 | }
19 |
20 | func (p *MemPiece) WriteAt(b []byte, off int64) (n int, err error) {
21 | p.mu.Lock()
22 | defer p.mu.Unlock()
23 |
24 | if p.buffer == nil {
25 | go p.piece.cache.cleanPieces()
26 | p.buffer = make([]byte, p.piece.cache.pieceLength, p.piece.cache.pieceLength)
27 | }
28 | n = copy(p.buffer[off:], b[:])
29 | p.piece.Size += int64(n)
30 | if p.piece.Size > p.piece.cache.pieceLength {
31 | p.piece.Size = p.piece.cache.pieceLength
32 | }
33 | p.piece.Accessed = time.Now().Unix()
34 | return
35 | }
36 |
37 | func (p *MemPiece) ReadAt(b []byte, off int64) (n int, err error) {
38 | p.mu.RLock()
39 | defer p.mu.RUnlock()
40 |
41 | size := len(b)
42 | if size+int(off) > len(p.buffer) {
43 | size = len(p.buffer) - int(off)
44 | if size < 0 {
45 | size = 0
46 | }
47 | }
48 | if len(p.buffer) < int(off) || len(p.buffer) < int(off)+size {
49 | return 0, io.EOF
50 | }
51 | n = copy(b, p.buffer[int(off) : int(off)+size][:])
52 | p.piece.Accessed = time.Now().Unix()
53 | if int64(len(b))+off >= p.piece.Size {
54 | go p.piece.cache.cleanPieces()
55 | }
56 | if n == 0 {
57 | return 0, io.EOF
58 | }
59 | return n, nil
60 | }
61 |
62 | func (p *MemPiece) Release() {
63 | p.mu.Lock()
64 | defer p.mu.Unlock()
65 | if p.buffer != nil {
66 | p.buffer = nil
67 | }
68 | p.piece.Size = 0
69 | p.piece.Complete = false
70 | }
71 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/piece.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "github.com/anacrolix/torrent"
5 | "github.com/anacrolix/torrent/storage"
6 | "server/settings"
7 | )
8 |
9 | type Piece struct {
10 | storage.PieceImpl `json:"-"`
11 |
12 | Id int `json:"-"`
13 | Size int64 `json:"size"`
14 |
15 | Complete bool `json:"complete"`
16 | Accessed int64 `json:"accessed"`
17 |
18 | mPiece *MemPiece `json:"-"`
19 | dPiece *DiskPiece `json:"-"`
20 |
21 | cache *Cache `json:"-"`
22 | }
23 |
24 | func NewPiece(id int, cache *Cache) *Piece {
25 | p := &Piece{
26 | Id: id,
27 | cache: cache,
28 | }
29 |
30 | if !settings.BTsets.UseDisk {
31 | p.mPiece = NewMemPiece(p)
32 | } else {
33 | p.dPiece = NewDiskPiece(p)
34 | }
35 | return p
36 | }
37 |
38 | func (p *Piece) WriteAt(b []byte, off int64) (n int, err error) {
39 | if !settings.BTsets.UseDisk {
40 | return p.mPiece.WriteAt(b, off)
41 | } else {
42 | return p.dPiece.WriteAt(b, off)
43 | }
44 | }
45 |
46 | func (p *Piece) ReadAt(b []byte, off int64) (n int, err error) {
47 | if !settings.BTsets.UseDisk {
48 | return p.mPiece.ReadAt(b, off)
49 | } else {
50 | return p.dPiece.ReadAt(b, off)
51 | }
52 | }
53 |
54 | func (p *Piece) MarkComplete() error {
55 | p.Complete = true
56 | return nil
57 | }
58 |
59 | func (p *Piece) MarkNotComplete() error {
60 | p.Complete = false
61 | return nil
62 | }
63 |
64 | func (p *Piece) Completion() storage.Completion {
65 | return storage.Completion{
66 | Complete: p.Complete,
67 | Ok: true,
68 | }
69 | }
70 |
71 | func (p *Piece) Release() {
72 | if !settings.BTsets.UseDisk {
73 | p.mPiece.Release()
74 | } else {
75 | p.dPiece.Release()
76 | }
77 | if !p.cache.isClosed {
78 | p.cache.torrent.Piece(p.Id).SetPriority(torrent.PiecePriorityNone)
79 | p.cache.torrent.Piece(p.Id).UpdateCompletion()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/piecefake.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/anacrolix/torrent/storage"
7 | )
8 |
9 | type PieceFake struct{}
10 |
11 | func (PieceFake) ReadAt(p []byte, off int64) (n int, err error) {
12 | err = errors.New("fake")
13 | return
14 | }
15 |
16 | func (PieceFake) WriteAt(p []byte, off int64) (n int, err error) {
17 | err = errors.New("fake")
18 | return
19 | }
20 |
21 | func (PieceFake) MarkComplete() error {
22 | return errors.New("fake")
23 | }
24 |
25 | func (PieceFake) MarkNotComplete() error {
26 | return errors.New("fake")
27 | }
28 |
29 | func (PieceFake) Completion() storage.Completion {
30 | return storage.Completion{
31 | Complete: false,
32 | Ok: true,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/ranges.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "sort"
5 |
6 | "github.com/anacrolix/torrent"
7 | )
8 |
9 | type Range struct {
10 | Start, End int
11 | File *torrent.File
12 | }
13 |
14 | func inRanges(ranges []Range, ind int) bool {
15 | for _, r := range ranges {
16 | if ind >= r.Start && ind <= r.End {
17 | return true
18 | }
19 | }
20 | return false
21 | }
22 |
23 | func mergeRange(ranges []Range) []Range {
24 | if len(ranges) <= 1 {
25 | return ranges
26 | }
27 | // copy ranges
28 | merged := append([]Range(nil), ranges...)
29 |
30 | sort.Slice(merged, func(i, j int) bool {
31 | if merged[i].Start < merged[j].Start {
32 | return true
33 | }
34 | if merged[i].Start == merged[j].Start && merged[i].End < merged[j].End {
35 | return true
36 | }
37 | return false
38 | })
39 |
40 | j := 0
41 | for i := 1; i < len(merged); i++ {
42 | if merged[j].End >= merged[i].Start {
43 | if merged[j].End < merged[i].End {
44 | merged[j].End = merged[i].End
45 | }
46 | } else {
47 | j++
48 | merged[j] = merged[i]
49 | }
50 | }
51 | return merged[:j+1]
52 | }
53 |
--------------------------------------------------------------------------------
/server/torr/storage/torrstor/storage.go:
--------------------------------------------------------------------------------
1 | package torrstor
2 |
3 | import (
4 | "sync"
5 |
6 | "server/torr/storage"
7 |
8 | "github.com/anacrolix/torrent/metainfo"
9 | ts "github.com/anacrolix/torrent/storage"
10 | )
11 |
12 | type Storage struct {
13 | storage.Storage
14 |
15 | caches map[metainfo.Hash]*Cache
16 | capacity int64
17 | mu sync.Mutex
18 | }
19 |
20 | func NewStorage(capacity int64) *Storage {
21 | stor := new(Storage)
22 | stor.capacity = capacity
23 | stor.caches = make(map[metainfo.Hash]*Cache)
24 | return stor
25 | }
26 |
27 | func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (ts.TorrentImpl, error) {
28 | // capFunc := func() (int64, bool) { // NE
29 | // return s.capacity, true // NE
30 | // } // NE
31 | s.mu.Lock()
32 | defer s.mu.Unlock()
33 | ch := NewCache(s.capacity, s)
34 | ch.Init(info, infoHash)
35 | s.caches[infoHash] = ch
36 | return ch, nil // OE
37 | // return ts.TorrentImpl{ // NE
38 | // Piece: ch.Piece, // NE
39 | // Close: ch.Close, // NE
40 | // Capacity: &capFunc, // NE
41 | // }, nil // NE
42 | }
43 |
44 | func (s *Storage) CloseHash(hash metainfo.Hash) {
45 | if s.caches == nil {
46 | return
47 | }
48 | s.mu.Lock()
49 | defer s.mu.Unlock()
50 | if ch, ok := s.caches[hash]; ok {
51 | ch.Close()
52 | delete(s.caches, hash)
53 | }
54 | }
55 |
56 | func (s *Storage) Close() error {
57 | s.mu.Lock()
58 | defer s.mu.Unlock()
59 | for _, ch := range s.caches {
60 | ch.Close()
61 | }
62 | return nil
63 | }
64 |
65 | func (s *Storage) GetCache(hash metainfo.Hash) *Cache {
66 | s.mu.Lock()
67 | defer s.mu.Unlock()
68 | if cache, ok := s.caches[hash]; ok {
69 | return cache
70 | }
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/server/torr/stream.go:
--------------------------------------------------------------------------------
1 | package torr
2 |
3 | import (
4 | "encoding/hex"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net"
9 | "net/http"
10 | "time"
11 |
12 | "github.com/anacrolix/dms/dlna"
13 | "github.com/anacrolix/missinggo/v2/httptoo"
14 | "github.com/anacrolix/torrent"
15 |
16 | mt "server/mimetype"
17 | sets "server/settings"
18 | "server/torr/state"
19 | )
20 |
21 | func (t *Torrent) Stream(fileID int, req *http.Request, resp http.ResponseWriter) error {
22 | if !t.GotInfo() {
23 | http.NotFound(resp, req)
24 | return errors.New("torrent don't get info")
25 | }
26 |
27 | st := t.Status()
28 | var stFile *state.TorrentFileStat
29 | for _, fileStat := range st.FileStats {
30 | if fileStat.Id == fileID {
31 | stFile = fileStat
32 | break
33 | }
34 | }
35 | if stFile == nil {
36 | return fmt.Errorf("file with id %v not found", fileID)
37 | }
38 |
39 | files := t.Files()
40 | var file *torrent.File
41 | for _, tfile := range files {
42 | if tfile.Path() == stFile.Path {
43 | file = tfile
44 | break
45 | }
46 | }
47 | if file == nil {
48 | return fmt.Errorf("file with id %v not found", fileID)
49 | }
50 | if int64(sets.MaxSize) > 0 && file.Length() > int64(sets.MaxSize) {
51 | log.Println("file", file.DisplayPath(), "size exceeded max allowed", sets.MaxSize, "bytes")
52 | return fmt.Errorf("file size exceeded max allowed %d bytes", sets.MaxSize)
53 | }
54 |
55 | reader := t.NewReader(file)
56 | if sets.BTsets.ResponsiveMode {
57 | reader.SetResponsive()
58 | }
59 |
60 | host, port, err := net.SplitHostPort(req.RemoteAddr)
61 | if sets.BTsets.EnableDebug {
62 | if err != nil {
63 | log.Println("Connect client")
64 | } else {
65 | log.Println("Connect client", host, port)
66 | }
67 | }
68 |
69 | sets.SetViewed(&sets.Viewed{Hash: t.Hash().HexString(), FileIndex: fileID})
70 |
71 | resp.Header().Set("Connection", "close")
72 | etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", t.Hash().HexString(), file.Path())))
73 | resp.Header().Set("ETag", httptoo.EncodeQuotedString(etag))
74 | // DLNA headers
75 | resp.Header().Set("transferMode.dlna.org", "Streaming")
76 | mime, err := mt.MimeTypeByPath(file.Path())
77 | if err == nil && mime.IsMedia() {
78 | resp.Header().Set("content-type", mime.String())
79 | }
80 | if req.Header.Get("getContentFeatures.dlna.org") != "" {
81 | resp.Header().Set("contentFeatures.dlna.org", dlna.ContentFeatures{
82 | SupportRange: true,
83 | SupportTimeSeek: true,
84 | }.String())
85 | }
86 |
87 | http.ServeContent(resp, req, file.Path(), time.Unix(t.Timestamp, 0), reader)
88 |
89 | t.CloseReader(reader)
90 | if sets.BTsets.EnableDebug {
91 | if err != nil {
92 | log.Println("Disconnect client")
93 | } else {
94 | log.Println("Disconnect client", host, port)
95 | }
96 | }
97 | return nil
98 | }
99 |
--------------------------------------------------------------------------------
/server/torr/utils/blockedIP.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | "path/filepath"
7 | "server/log"
8 | "strings"
9 |
10 | "server/settings"
11 |
12 | "github.com/anacrolix/torrent/iplist"
13 | )
14 |
15 | func ReadBlockedIP() (ranger iplist.Ranger, err error) {
16 | buf, err := os.ReadFile(filepath.Join(settings.Path, "blocklist"))
17 | if err != nil {
18 | return nil, err
19 | }
20 | log.TLogln("Read block list...")
21 | scanner := bufio.NewScanner(strings.NewReader(string(buf)))
22 | var ranges []iplist.Range
23 | for scanner.Scan() {
24 | r, ok, err := iplist.ParseBlocklistP2PLine(scanner.Bytes())
25 | if err != nil {
26 | return nil, err
27 | }
28 | if ok {
29 | ranges = append(ranges, r)
30 | }
31 | }
32 | err = scanner.Err()
33 | if len(ranges) > 0 {
34 | ranger = iplist.New(ranges)
35 | log.TLogln("Readed ranges:", len(ranges))
36 | }
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/server/torr/utils/freemem.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "runtime"
5 | "runtime/debug"
6 | )
7 |
8 | func FreeOSMem() {
9 | debug.FreeOSMemory()
10 | }
11 |
12 | func FreeOSMemGC() {
13 | runtime.GC()
14 | debug.FreeOSMemory()
15 | }
16 |
--------------------------------------------------------------------------------
/server/torr/utils/torrent.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/base32"
5 | "io"
6 | "math/rand"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "server/settings"
13 |
14 | "golang.org/x/time/rate"
15 | )
16 |
17 | var defTrackers = []string{
18 | "http://retracker.local/announce",
19 | "http://bt4.t-ru.org/ann?magnet",
20 | "http://retracker.mgts.by:80/announce",
21 | "http://tracker.city9x.com:2710/announce",
22 | "http://tracker.electro-torrent.pl:80/announce",
23 | "http://tracker.internetwarriors.net:1337/announce",
24 | "http://tracker2.itzmx.com:6961/announce",
25 | "udp://opentor.org:2710",
26 | "udp://public.popcorn-tracker.org:6969/announce",
27 | "udp://tracker.opentrackr.org:1337/announce",
28 | "http://bt.svao-ix.ru/announce",
29 | "udp://explodie.org:6969/announce",
30 | "wss://tracker.btorrent.xyz",
31 | "wss://tracker.openwebtorrent.com",
32 | }
33 |
34 | var loadedTrackers []string
35 |
36 | func GetTrackerFromFile() []string {
37 | name := filepath.Join(settings.Path, "trackers.txt")
38 | buf, err := os.ReadFile(name)
39 | if err == nil {
40 | list := strings.Split(string(buf), "\n")
41 | var ret []string
42 | for _, l := range list {
43 | if strings.HasPrefix(l, "udp") || strings.HasPrefix(l, "http") {
44 | ret = append(ret, l)
45 | }
46 | }
47 | return ret
48 | }
49 | return nil
50 | }
51 |
52 | func GetDefTrackers() []string {
53 | loadNewTracker()
54 | if len(loadedTrackers) == 0 {
55 | return defTrackers
56 | }
57 | return loadedTrackers
58 | }
59 |
60 | func loadNewTracker() {
61 | if len(loadedTrackers) > 0 {
62 | return
63 | }
64 | resp, err := http.Get("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt")
65 | if err == nil {
66 | defer resp.Body.Close()
67 | buf, err := io.ReadAll(resp.Body)
68 | if err == nil {
69 | arr := strings.Split(string(buf), "\n")
70 | var ret []string
71 | for _, s := range arr {
72 | s = strings.TrimSpace(s)
73 | if len(s) > 0 {
74 | ret = append(ret, s)
75 | }
76 | }
77 | loadedTrackers = append(ret, defTrackers...)
78 | }
79 | }
80 | }
81 |
82 | func PeerIDRandom(peer string) string {
83 | randomBytes := make([]byte, 32)
84 | _, err := rand.Read(randomBytes)
85 | if err != nil {
86 | panic(err)
87 | }
88 | return peer + base32.StdEncoding.EncodeToString(randomBytes)[:20-len(peer)]
89 | }
90 |
91 | func Limit(i int) *rate.Limiter {
92 | l := rate.NewLimiter(rate.Inf, 0)
93 | if i > 0 {
94 | b := i
95 | if b < 16*1024 {
96 | b = 16 * 1024
97 | }
98 | l = rate.NewLimiter(rate.Limit(i), b)
99 | }
100 | return l
101 | }
102 |
--------------------------------------------------------------------------------
/server/torr/utils/webImageChecker.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "image"
5 | _ "image/gif"
6 | _ "image/jpeg"
7 | _ "image/png"
8 | "net/http"
9 | "strings"
10 |
11 | "golang.org/x/image/webp"
12 |
13 | "server/log"
14 | )
15 |
16 | func CheckImgUrl(link string) bool {
17 | if link == "" {
18 | return false
19 | }
20 | resp, err := http.Get(link)
21 | if err != nil {
22 | log.TLogln("Error check image:", err)
23 | return false
24 | }
25 | defer resp.Body.Close()
26 | if strings.HasSuffix(link, ".webp") {
27 | _, err = webp.Decode(resp.Body)
28 | } else {
29 | _, _, err = image.Decode(resp.Body)
30 | }
31 | if err != nil {
32 | log.TLogln("Error decode image:", err)
33 | return false
34 | }
35 | return err == nil
36 | }
37 |
--------------------------------------------------------------------------------
/server/utils/filetypes.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 |
7 | "server/torr/state"
8 | )
9 |
10 | var extVideo = map[string]interface{}{
11 | ".3g2": nil,
12 | ".3gp": nil,
13 | ".aaf": nil,
14 | ".asf": nil,
15 | ".avchd": nil,
16 | ".avi": nil,
17 | ".drc": nil,
18 | ".flv": nil,
19 | ".m2ts": nil,
20 | ".m2v": nil,
21 | ".m4p": nil,
22 | ".m4v": nil,
23 | ".mkv": nil,
24 | ".mng": nil,
25 | ".mov": nil,
26 | ".mp2": nil,
27 | ".mp4": nil,
28 | ".mpe": nil,
29 | ".mpeg": nil,
30 | ".mpg": nil,
31 | ".mpv": nil,
32 | ".mts": nil,
33 | ".mxf": nil,
34 | ".nsv": nil,
35 | ".ogg": nil,
36 | ".ogv": nil,
37 | ".qt": nil,
38 | ".rm": nil,
39 | ".rmvb": nil,
40 | ".roq": nil,
41 | ".svi": nil,
42 | ".ts": nil,
43 | ".vob": nil,
44 | ".webm": nil,
45 | ".wmv": nil,
46 | ".yuv": nil,
47 | }
48 |
49 | var extAudio = map[string]interface{}{
50 | ".aac": nil,
51 | ".aiff": nil,
52 | ".ape": nil,
53 | ".au": nil,
54 | ".dff": nil,
55 | ".dsd": nil,
56 | ".dsf": nil,
57 | ".flac": nil,
58 | ".gsm": nil,
59 | ".it": nil,
60 | ".m3u": nil,
61 | ".m4a": nil,
62 | ".mid": nil,
63 | ".mod": nil,
64 | ".mp3": nil,
65 | ".mpa": nil,
66 | ".mpga": nil,
67 | ".oga": nil,
68 | ".ogg": nil,
69 | ".opus": nil,
70 | ".pls": nil,
71 | ".ra": nil,
72 | ".s3m": nil,
73 | ".sid": nil,
74 | ".spx": nil,
75 | ".wav": nil,
76 | ".wma": nil,
77 | ".xm": nil,
78 | }
79 |
80 | func GetMimeType(filename string) string {
81 | ext := strings.ToLower(filepath.Ext(filename))
82 | if _, ok := extVideo[ext]; ok {
83 | return "video/*"
84 | }
85 | if _, ok := extAudio[ext]; ok {
86 | return "audio/*"
87 | }
88 | return "*/*"
89 | }
90 |
91 | func GetPlayableFiles(st state.TorrentStatus) []*state.TorrentFileStat {
92 | files := make([]*state.TorrentFileStat, 0)
93 | for _, f := range st.FileStats {
94 | if GetMimeType(f.Path) != "*/*" {
95 | files = append(files, f)
96 | }
97 | }
98 | return files
99 | }
100 |
--------------------------------------------------------------------------------
/server/utils/location.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/gin-contrib/location"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | func GetScheme(c *gin.Context) string {
9 | url := location.Get(c)
10 | if url == nil {
11 | return "http"
12 | }
13 | return url.Scheme
14 | }
15 |
--------------------------------------------------------------------------------
/server/utils/prallel.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | func ParallelFor(begin, end int, fn func(i int)) {
8 | var wg sync.WaitGroup
9 | wg.Add(end - begin)
10 | for i := begin; i < end; i++ {
11 | go func(i int) {
12 | fn(i)
13 | wg.Done()
14 | }(i)
15 | }
16 | wg.Wait()
17 | }
18 |
--------------------------------------------------------------------------------
/server/utils/strings.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "unicode"
8 | )
9 |
10 | const (
11 | _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier
12 | KB
13 | MB
14 | GB
15 | TB
16 | PB
17 | EB
18 | )
19 |
20 | func Format(b float64) string {
21 | multiple := ""
22 | value := b
23 |
24 | switch {
25 | case b >= EB:
26 | value /= EB
27 | multiple = "EB"
28 | case b >= PB:
29 | value /= PB
30 | multiple = "PB"
31 | case b >= TB:
32 | value /= TB
33 | multiple = "TB"
34 | case b >= GB:
35 | value /= GB
36 | multiple = "GB"
37 | case b >= MB:
38 | value /= MB
39 | multiple = "MB"
40 | case b >= KB:
41 | value /= KB
42 | multiple = "KB"
43 | case b == 0:
44 | return "0"
45 | default:
46 | return strconv.FormatInt(int64(b), 10) + "B"
47 | }
48 |
49 | return fmt.Sprintf("%.2f%s", value, multiple)
50 | }
51 |
52 | func CommonPrefix(first, second string) string {
53 | var result strings.Builder
54 |
55 | minLength := len(first)
56 | if len(second) < minLength {
57 | minLength = len(second)
58 | }
59 |
60 | for i := 0; i < minLength; i++ {
61 | if first[i] != second[i] {
62 | break
63 | }
64 | result.WriteByte(first[i])
65 | }
66 |
67 | return result.String()
68 | }
69 |
70 | func NumberPrefix(str string) (int, error) {
71 | var result strings.Builder
72 |
73 | for i := 0; i < len(str); i++ {
74 | if !unicode.IsDigit(rune(str[i])) {
75 | break
76 | }
77 | result.WriteByte(str[i])
78 | }
79 |
80 | return strconv.Atoi(result.String())
81 | }
82 |
83 | func CompareStrings(first, second string) bool {
84 | commonPrefix := CommonPrefix(first, second)
85 | resultStr1 := strings.TrimPrefix(first, commonPrefix)
86 | resultStr2 := strings.TrimPrefix(second, commonPrefix)
87 | num1, err1 := NumberPrefix(resultStr1)
88 | num2, err2 := NumberPrefix(resultStr2)
89 |
90 | if err1 == nil && err2 == nil {
91 | return num1 < num2
92 | }
93 | if err1 == nil {
94 | return true
95 | } else if err2 == nil {
96 | return false
97 | }
98 | return resultStr1 < resultStr2
99 | }
100 |
--------------------------------------------------------------------------------
/server/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "log"
5 | "runtime/debug"
6 | )
7 |
8 | const Version = "MatriX.135"
9 |
10 | func GetTorrentVersion() string {
11 | bi, ok := debug.ReadBuildInfo()
12 | if !ok {
13 | log.Printf("Failed to read build info")
14 | return ""
15 | }
16 | for _, dep := range bi.Deps {
17 | if dep.Path == "github.com/anacrolix/torrent" {
18 | if dep.Replace != nil {
19 | return dep.Replace.Version
20 | } else {
21 | return dep.Version
22 | }
23 | }
24 | }
25 | return ""
26 | }
27 |
--------------------------------------------------------------------------------
/server/web/api/cache.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "server/torr"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | // Action: get
13 | type cacheReqJS struct {
14 | requestI
15 | Hash string `json:"hash,omitempty"`
16 | }
17 |
18 | // cache godoc
19 | //
20 | // @Summary Return cache stats
21 | // @Description Return cache stats.
22 | //
23 | // @Tags API
24 | //
25 | // @Param request body cacheReqJS true "Cache stats request"
26 | //
27 | // @Produce json
28 | // @Success 200 {object} state.CacheState "Cache stats"
29 | // @Router /cache [post]
30 | func cache(c *gin.Context) {
31 | var req cacheReqJS
32 | err := c.ShouldBindJSON(&req)
33 | if err != nil {
34 | c.AbortWithError(http.StatusBadRequest, err)
35 | return
36 | }
37 | c.Status(http.StatusBadRequest)
38 | switch req.Action {
39 | case "get":
40 | {
41 | getCache(req, c)
42 | }
43 | }
44 | }
45 |
46 | func getCache(req cacheReqJS, c *gin.Context) {
47 | if req.Hash == "" {
48 | c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
49 | return
50 | }
51 | tor := torr.GetTorrent(req.Hash)
52 |
53 | if tor != nil {
54 | st := tor.CacheState()
55 | if st == nil {
56 | c.JSON(200, struct{}{})
57 | } else {
58 | c.JSON(200, st)
59 | }
60 | } else {
61 | c.Status(http.StatusNotFound)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/server/web/api/download.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | type fileReader struct {
14 | pos int64
15 | size int64
16 | io.ReadSeeker
17 | }
18 |
19 | func newFR(size int64) *fileReader {
20 | return &fileReader{
21 | pos: 0,
22 | size: size,
23 | }
24 | }
25 |
26 | func (f *fileReader) Read(p []byte) (n int, err error) {
27 | f.pos = f.pos + int64(len(p))
28 | return len(p), nil
29 | }
30 |
31 | func (f *fileReader) Seek(offset int64, whence int) (int64, error) {
32 | switch whence {
33 | case 0:
34 | f.pos = offset
35 | case 1:
36 | f.pos += offset
37 | case 2:
38 | f.pos = f.size + offset
39 | }
40 | return f.pos, nil
41 | }
42 |
43 | // download godoc
44 | //
45 | // @Summary Generates test file of given size
46 | // @Description Download the test file of given size (for speed testing purpose).
47 | //
48 | // @Tags API
49 | //
50 | // @Param size path string true "Test file size (in MB)"
51 | //
52 | // @Produce application/octet-stream
53 | // @Success 200 {file} file
54 | // @Router /download/{size} [get]
55 | func download(c *gin.Context) {
56 | szStr := c.Param("size")
57 | sz, err := strconv.Atoi(szStr)
58 | if err != nil {
59 | c.Error(err)
60 | return
61 | }
62 |
63 | http.ServeContent(c.Writer, c.Request, fmt.Sprintln(szStr)+"mb.bin", time.Now(), newFR(int64(sz*1024*1024)))
64 | }
65 |
--------------------------------------------------------------------------------
/server/web/api/ffprobe.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 |
8 | "server/ffprobe"
9 | sets "server/settings"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // ffp godoc
15 | //
16 | // @Summary Gather informations using ffprobe
17 | // @Description Gather informations using ffprobe.
18 | //
19 | // @Tags API
20 | //
21 | // @Param hash path string true "Torrent hash"
22 | // @Param id path string true "File index in torrent"
23 | //
24 | // @Produce json
25 | // @Success 200 "Data returned from ffprobe"
26 | // @Router /ffp/{hash}/{id} [get]
27 | func ffp(c *gin.Context) {
28 | hash := c.Param("hash")
29 | indexStr := c.Param("id")
30 |
31 | if hash == "" || indexStr == "" {
32 | c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty"))
33 | return
34 | }
35 |
36 | link := "http://127.0.0.1:" + sets.Port + "/play/" + hash + "/" + indexStr
37 | if sets.Ssl {
38 | link = "https://127.0.0.1:" + sets.SslPort + "/play/" + hash + "/" + indexStr
39 | }
40 |
41 | data, err := ffprobe.ProbeUrl(link)
42 | if err != nil {
43 | c.AbortWithError(http.StatusBadRequest, fmt.Errorf("error getting data: %v", err))
44 | return
45 | }
46 |
47 | c.JSON(200, data)
48 | }
49 |
--------------------------------------------------------------------------------
/server/web/api/play.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/gin-gonic/gin"
9 |
10 | "server/torr"
11 | "server/torr/state"
12 | "server/web/api/utils"
13 | )
14 |
15 | // play godoc
16 | //
17 | // @Summary Play given torrent referenced by hash
18 | // @Description Play given torrent referenced by hash.
19 | //
20 | // @Tags API
21 | //
22 | // @Param hash path string true "Torrent hash"
23 | // @Param id path string true "File index in torrent"
24 | //
25 | // @Produce application/octet-stream
26 | // @Success 200 "Torrent data"
27 | // @Router /play/{hash}/{id} [get]
28 | func play(c *gin.Context) {
29 | hash := c.Param("hash")
30 | indexStr := c.Param("id")
31 | notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == ""
32 |
33 | if hash == "" || indexStr == "" {
34 | c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty"))
35 | return
36 | }
37 |
38 | spec, err := utils.ParseLink(hash)
39 | if err != nil {
40 | c.AbortWithError(http.StatusInternalServerError, err)
41 | return
42 | }
43 |
44 | tor := torr.GetTorrent(spec.InfoHash.HexString())
45 | if tor == nil && notAuth {
46 | c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
47 | c.AbortWithStatus(http.StatusUnauthorized)
48 | return
49 | }
50 |
51 | if tor == nil {
52 | c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent"))
53 | return
54 | }
55 |
56 | if tor.Stat == state.TorrentInDB {
57 | tor, err = torr.AddTorrent(spec, tor.Title, tor.Poster, tor.Data, tor.Category)
58 | if err != nil {
59 | c.AbortWithError(http.StatusInternalServerError, err)
60 | return
61 | }
62 | }
63 |
64 | if !tor.GotInfo() {
65 | c.AbortWithError(http.StatusInternalServerError, errors.New("timeout connection torrent"))
66 | return
67 | }
68 |
69 | // find file
70 | index := -1
71 | if len(tor.Files()) == 1 {
72 | index = 1
73 | } else {
74 | ind, err := strconv.Atoi(indexStr)
75 | if err == nil {
76 | index = ind
77 | }
78 | }
79 | if index == -1 { // if file index not set and play file exec
80 | c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is wrong"))
81 | return
82 | }
83 |
84 | tor.Stream(index, c.Request, c.Writer)
85 | }
86 |
--------------------------------------------------------------------------------
/server/web/api/route.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | config "server/settings"
5 | "server/web/auth"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | type requestI struct {
11 | Action string `json:"action,omitempty"`
12 | }
13 |
14 | func SetupRoute(route gin.IRouter) {
15 | authorized := route.Group("/", auth.CheckAuth())
16 |
17 | authorized.GET("/shutdown", shutdown)
18 | authorized.GET("/shutdown/*reason", shutdown)
19 |
20 | authorized.POST("/settings", settings)
21 |
22 | authorized.POST("/torrents", torrents)
23 |
24 | authorized.POST("/torrent/upload", torrentUpload)
25 |
26 | authorized.POST("/cache", cache)
27 |
28 | route.HEAD("/stream", stream)
29 | route.GET("/stream", stream)
30 |
31 | route.HEAD("/stream/*fname", stream)
32 | route.GET("/stream/*fname", stream)
33 |
34 | route.HEAD("/play/:hash/:id", play)
35 | route.GET("/play/:hash/:id", play)
36 |
37 | authorized.POST("/viewed", viewed)
38 |
39 | authorized.GET("/playlistall/all.m3u", allPlayList)
40 |
41 | route.GET("/playlist", playList)
42 | route.GET("/playlist/*fname", playList)
43 |
44 | authorized.GET("/download/:size", download)
45 |
46 | if config.SearchWA {
47 | route.GET("/search/*query", rutorSearch)
48 | } else {
49 | authorized.GET("/search/*query", rutorSearch)
50 | }
51 |
52 | authorized.GET("/ffp/:hash/:id", ffp)
53 | }
54 |
--------------------------------------------------------------------------------
/server/web/api/rutor.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 |
7 | "github.com/gin-gonic/gin"
8 |
9 | "server/rutor"
10 | "server/rutor/models"
11 | sets "server/settings"
12 | )
13 |
14 | // rutorSearch godoc
15 | //
16 | // @Summary Makes a rutor search
17 | // @Description Makes a rutor search.
18 | //
19 | // @Tags API
20 | //
21 | // @Param query query string true "Rutor query"
22 | //
23 | // @Produce json
24 | // @Success 200 {array} models.TorrentDetails "Rutor torrent search result(s)"
25 | // @Router /search [get]
26 | func rutorSearch(c *gin.Context) {
27 | if !sets.BTsets.EnableRutorSearch {
28 | c.JSON(http.StatusBadRequest, []string{})
29 | return
30 | }
31 | query := c.Query("query")
32 | query, _ = url.QueryUnescape(query)
33 | list := rutor.Search(query)
34 | if list == nil {
35 | list = []*models.TorrentDetails{}
36 | }
37 | c.JSON(200, list)
38 | }
39 |
--------------------------------------------------------------------------------
/server/web/api/settings.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "server/rutor"
7 |
8 | "server/dlna"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/pkg/errors"
12 |
13 | sets "server/settings"
14 | "server/torr"
15 | )
16 |
17 | // Action: get, set, def
18 | type setsReqJS struct {
19 | requestI
20 | Sets *sets.BTSets `json:"sets,omitempty"`
21 | }
22 |
23 | // settings godoc
24 | //
25 | // @Summary Get / Set server settings
26 | // @Description Allow to get or set server settings.
27 | //
28 | // @Tags API
29 | //
30 | // @Param request body setsReqJS true "Settings request. Available params for action: get, set, def"
31 | //
32 | // @Accept json
33 | // @Produce json
34 | // @Success 200 {object} sets.BTSets "Settings JSON or nothing. Depends on what action has been asked."
35 | // @Router /settings [post]
36 | func settings(c *gin.Context) {
37 | var req setsReqJS
38 | err := c.ShouldBindJSON(&req)
39 | if err != nil {
40 | c.AbortWithError(http.StatusBadRequest, err)
41 | return
42 | }
43 |
44 | if req.Action == "get" {
45 | c.JSON(200, sets.BTsets)
46 | return
47 | } else if req.Action == "set" {
48 | torr.SetSettings(req.Sets)
49 | dlna.Stop()
50 | if req.Sets.EnableDLNA {
51 | dlna.Start()
52 | }
53 | rutor.Stop()
54 | rutor.Start()
55 | c.Status(200)
56 | return
57 | } else if req.Action == "def" {
58 | torr.SetDefSettings()
59 | dlna.Stop()
60 | rutor.Stop()
61 | c.Status(200)
62 | return
63 | }
64 | c.AbortWithError(http.StatusBadRequest, errors.New("action is empty"))
65 | }
66 |
--------------------------------------------------------------------------------
/server/web/api/shutdown.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "time"
7 |
8 | sets "server/settings"
9 | "server/torr"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // shutdown godoc
15 | // @Summary Shuts down server
16 | // @Description Gracefully shuts down server after 1 second.
17 | //
18 | // @Tags API
19 | //
20 | // @Success 200
21 | // @Router /shutdown [get]
22 | func shutdown(c *gin.Context) {
23 | reasonStr := strings.ReplaceAll(c.Param("reason"), `/`, "")
24 | if sets.ReadOnly && reasonStr == "" {
25 | c.Status(http.StatusForbidden)
26 | return
27 | }
28 | c.Status(200)
29 | go func() {
30 | time.Sleep(1000)
31 | torr.Shutdown()
32 | }()
33 | }
34 |
--------------------------------------------------------------------------------
/server/web/api/upload.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "server/log"
7 | set "server/settings"
8 | "server/torr"
9 | "server/web/api/utils"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // torrentUpload godoc
15 | //
16 | // @Summary Add .torrent file
17 | // @Description Only one file support.
18 | //
19 | // @Tags API
20 | //
21 | // @Param file formData file true "Torrent file to insert"
22 | // @Param save formData string false "Save to DB"
23 | // @Param title formData string false "Torrent title"
24 | // @Param category formData string false "Torrent category"
25 | // @Param poster formData string false "Torrent poster"
26 | // @Param data formData string false "Torrent data"
27 | //
28 | // @Accept multipart/form-data
29 | //
30 | // @Produce json
31 | // @Success 200 {object} state.TorrentStatus "Torrent status"
32 | // @Router /torrent/upload [post]
33 | func torrentUpload(c *gin.Context) {
34 | form, err := c.MultipartForm()
35 | if err != nil {
36 | c.AbortWithError(http.StatusBadRequest, err)
37 | return
38 | }
39 | defer form.RemoveAll()
40 |
41 | save := len(form.Value["save"]) > 0
42 | title := ""
43 | if len(form.Value["title"]) > 0 {
44 | title = form.Value["title"][0]
45 | }
46 | category := ""
47 | if len(form.Value["category"]) > 0 {
48 | category = form.Value["category"][0]
49 | }
50 | poster := ""
51 | if len(form.Value["poster"]) > 0 {
52 | poster = form.Value["poster"][0]
53 | }
54 | data := ""
55 | if len(form.Value["data"]) > 0 {
56 | data = form.Value["data"][0]
57 | }
58 | var tor *torr.Torrent
59 | for name, file := range form.File {
60 | log.TLogln("add .torrent", name)
61 |
62 | torrFile, err := file[0].Open()
63 | if err != nil {
64 | log.TLogln("error upload torrent:", err)
65 | continue
66 | }
67 | defer torrFile.Close()
68 |
69 | spec, err := utils.ParseFile(torrFile)
70 | if err != nil {
71 | log.TLogln("error upload torrent:", err)
72 | continue
73 | }
74 |
75 | tor, err = torr.AddTorrent(spec, title, poster, data, category)
76 |
77 | if tor.Data != "" && set.BTsets.EnableDebug {
78 | log.TLogln("torrent data:", tor.Data)
79 | }
80 | if tor.Category != "" && set.BTsets.EnableDebug {
81 | log.TLogln("torrent category:", tor.Category)
82 | }
83 |
84 | if err != nil {
85 | log.TLogln("error upload torrent:", err)
86 | continue
87 | }
88 |
89 | go func() {
90 | if !tor.GotInfo() {
91 | log.TLogln("error add torrent:", "timeout connection torrent")
92 | return
93 | }
94 |
95 | if tor.Title == "" {
96 | tor.Title = tor.Name()
97 | }
98 |
99 | if save {
100 | torr.SaveTorrentToDB(tor)
101 | }
102 | }()
103 |
104 | break
105 | }
106 | c.JSON(200, tor.Status())
107 | }
108 |
--------------------------------------------------------------------------------
/server/web/api/viewed.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | sets "server/settings"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | /*
12 | file index starts from 1
13 | */
14 |
15 | // Action: set, rem, list
16 | type viewedReqJS struct {
17 | requestI
18 | *sets.Viewed
19 | }
20 |
21 | // viewed godoc
22 | //
23 | // @Summary Set / List / Remove viewed torrents
24 | // @Description Allow to set, list or remove viewed torrents from server.
25 | //
26 | // @Tags API
27 | //
28 | // @Param request body viewedReqJS true "Viewed torrent request. Available params for action: set, rem, list"
29 | //
30 | // @Accept json
31 | // @Produce json
32 | // @Success 200 {array} sets.Viewed
33 | // @Router /viewed [post]
34 | func viewed(c *gin.Context) {
35 | var req viewedReqJS
36 | err := c.ShouldBindJSON(&req)
37 | if err != nil {
38 | c.AbortWithError(http.StatusBadRequest, err)
39 | return
40 | }
41 |
42 | switch req.Action {
43 | case "set":
44 | {
45 | setViewed(req, c)
46 | }
47 | case "rem":
48 | {
49 | remViewed(req, c)
50 | }
51 | case "list":
52 | {
53 | listViewed(req, c)
54 | }
55 | }
56 | }
57 |
58 | func setViewed(req viewedReqJS, c *gin.Context) {
59 | sets.SetViewed(req.Viewed)
60 | c.Status(200)
61 | }
62 |
63 | func remViewed(req viewedReqJS, c *gin.Context) {
64 | sets.RemViewed(req.Viewed)
65 | c.Status(200)
66 | }
67 |
68 | func listViewed(req viewedReqJS, c *gin.Context) {
69 | list := sets.ListViewed(req.Hash)
70 | c.JSON(200, list)
71 | }
72 |
--------------------------------------------------------------------------------
/server/web/auth/auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "net/http"
7 | "os"
8 | "path/filepath"
9 | "unsafe"
10 |
11 | "github.com/gin-gonic/gin"
12 |
13 | "server/log"
14 | "server/settings"
15 | )
16 |
17 | func SetupAuth(engine *gin.Engine) {
18 | if !settings.HttpAuth {
19 | return
20 | }
21 | accs := getAccounts()
22 | if accs == nil {
23 | return
24 | }
25 | engine.Use(BasicAuth(accs))
26 | }
27 |
28 | func getAccounts() gin.Accounts {
29 | buf, err := os.ReadFile(filepath.Join(settings.Path, "accs.db"))
30 | if err != nil {
31 | return nil
32 | }
33 | var accs gin.Accounts
34 | err = json.Unmarshal(buf, &accs)
35 | if err != nil {
36 | log.TLogln("Error parse accs.db", err)
37 | }
38 | return accs
39 | }
40 |
41 | type authPair struct {
42 | value string
43 | user string
44 | }
45 | type authPairs []authPair
46 |
47 | func (a authPairs) searchCredential(authValue string) (string, bool) {
48 | if authValue == "" {
49 | return "", false
50 | }
51 | for _, pair := range a {
52 | if pair.value == authValue {
53 | return pair.user, true
54 | }
55 | }
56 | return "", false
57 | }
58 |
59 | func BasicAuth(accounts gin.Accounts) gin.HandlerFunc {
60 | pairs := processAccounts(accounts)
61 | return func(c *gin.Context) {
62 | c.Set("auth_required", true)
63 |
64 | user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
65 | if found {
66 | c.Set(gin.AuthUserKey, user)
67 | }
68 | }
69 | }
70 |
71 | func CheckAuth() gin.HandlerFunc {
72 | return func(c *gin.Context) {
73 | if !settings.HttpAuth {
74 | return
75 | }
76 |
77 | if _, ok := c.Get(gin.AuthUserKey); ok {
78 | return
79 | }
80 |
81 | c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
82 | c.AbortWithStatus(http.StatusUnauthorized)
83 | }
84 | }
85 |
86 | func processAccounts(accounts gin.Accounts) authPairs {
87 | pairs := make(authPairs, 0, len(accounts))
88 | for user, password := range accounts {
89 | value := authorizationHeader(user, password)
90 | pairs = append(pairs, authPair{
91 | value: value,
92 | user: user,
93 | })
94 | }
95 | return pairs
96 | }
97 |
98 | func authorizationHeader(user, password string) string {
99 | base := user + ":" + password
100 | return "Basic " + base64.StdEncoding.EncodeToString(StringToBytes(base))
101 | }
102 |
103 | func StringToBytes(s string) (b []byte) {
104 | return unsafe.Slice(unsafe.StringData(s), len(s))
105 | }
106 |
--------------------------------------------------------------------------------
/server/web/blocker/blocker.go:
--------------------------------------------------------------------------------
1 | package blocker
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "net"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 |
13 | "server/log"
14 | "server/settings"
15 |
16 | "github.com/gin-gonic/gin"
17 | )
18 |
19 | func Blocker() gin.HandlerFunc {
20 | emptyFN := func(c *gin.Context) {
21 | c.Next()
22 | }
23 |
24 | name := filepath.Join(settings.Path, "bip.txt")
25 | buf, _ := os.ReadFile(name)
26 | blackIpList := scanBuf(buf)
27 |
28 | name = filepath.Join(settings.Path, "wip.txt")
29 | buf, _ = os.ReadFile(name)
30 | whiteIpList := scanBuf(buf)
31 |
32 | if blackIpList.NumRanges() == 0 && whiteIpList.NumRanges() == 0 {
33 | return emptyFN
34 | }
35 |
36 | return func(c *gin.Context) {
37 | arr := strings.Split(c.Request.RemoteAddr, ":")
38 | if len(arr) > 0 {
39 | ip := net.ParseIP(arr[0])
40 | minifyIP(&ip)
41 | if whiteIpList.NumRanges() > 0 {
42 | if _, ok := whiteIpList.Lookup(ip); !ok {
43 | log.WebLogln("Block ip, not in white list", ip.String())
44 | c.String(http.StatusTeapot, "Banned")
45 | c.Abort()
46 | return
47 | }
48 | }
49 | if blackIpList.NumRanges() > 0 {
50 | if r, ok := blackIpList.Lookup(ip); ok {
51 | log.WebLogln("Block ip, in black list:", ip.String(), "in range", r.Description, ":", r.First, "-", r.Last)
52 | c.String(http.StatusTeapot, "Banned")
53 | c.Abort()
54 | return
55 | }
56 | }
57 | }
58 | c.Next()
59 | }
60 | }
61 |
62 | func scanBuf(buf []byte) Ranger {
63 | if len(buf) == 0 {
64 | return New(nil)
65 | }
66 | var ranges []Range
67 | scanner := bufio.NewScanner(strings.NewReader(string(buf)))
68 | for scanner.Scan() {
69 | r, ok, err := parseLine(scanner.Bytes())
70 | if err != nil {
71 | log.TLogln("Error scan ip list:", err)
72 | return New(nil)
73 | }
74 | if ok {
75 | ranges = append(ranges, r)
76 | }
77 | }
78 | err := scanner.Err()
79 | if err != nil {
80 | log.TLogln("Error scan ip list:", err)
81 | }
82 | if len(ranges) > 0 {
83 | return New(ranges)
84 | }
85 | return New(nil)
86 | }
87 |
88 | func parseLine(l []byte) (r Range, ok bool, err error) {
89 | l = bytes.TrimSpace(l)
90 | if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) {
91 | return
92 | }
93 | colon := bytes.LastIndexAny(l, ":")
94 | hyphen := bytes.IndexByte(l[colon+1:], '-')
95 | hyphen += colon + 1
96 | if colon >= 0 {
97 | r.Description = string(l[:colon])
98 | }
99 | if hyphen-(colon+1) >= 0 {
100 | r.First = net.ParseIP(string(l[colon+1 : hyphen]))
101 | minifyIP(&r.First)
102 | r.Last = net.ParseIP(string(l[hyphen+1:]))
103 | minifyIP(&r.Last)
104 | } else {
105 | r.First = net.ParseIP(string(l[colon+1:]))
106 | minifyIP(&r.First)
107 | r.Last = r.First
108 | }
109 | if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) {
110 | err = errors.New("bad IP range")
111 | return
112 | }
113 | ok = true
114 | return
115 | }
116 |
--------------------------------------------------------------------------------
/server/web/blocker/iplist.go:
--------------------------------------------------------------------------------
1 | package blocker
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net"
7 | )
8 |
9 | type Ranger interface {
10 | Lookup(net.IP) (r Range, ok bool)
11 | NumRanges() int
12 | }
13 |
14 | type IPList struct {
15 | ranges []Range
16 | }
17 |
18 | type Range struct {
19 | First, Last net.IP
20 | Description string
21 | }
22 |
23 | func (r Range) String() string {
24 | return fmt.Sprintf("%s-%s: %s", r.First, r.Last, r.Description)
25 | }
26 |
27 | // Create a new IP list. The given ranges must already sorted by the lower
28 | // bound IP in each range. Behaviour is undefined for lists of overlapping
29 | // ranges.
30 | func New(initSorted []Range) *IPList {
31 | return &IPList{
32 | ranges: initSorted,
33 | }
34 | }
35 |
36 | func (ipl *IPList) NumRanges() int {
37 | if ipl == nil {
38 | return 0
39 | }
40 | return len(ipl.ranges)
41 | }
42 |
43 | // Return the range the given IP is in. ok if false if no range is found.
44 | func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) {
45 | if ipl == nil {
46 | return
47 | }
48 | v4 := ip.To4()
49 | if v4 != nil {
50 | r, ok = ipl.lookup(v4)
51 | if ok {
52 | return
53 | }
54 | }
55 | v6 := ip.To16()
56 | if v6 != nil {
57 | return ipl.lookup(v6)
58 | }
59 | if v4 == nil && v6 == nil {
60 | r = Range{
61 | Description: "bad IP",
62 | }
63 | ok = true
64 | }
65 | return
66 | }
67 |
68 | // Return the range the given IP is in. Returns nil if no range is found.
69 | func (ipl *IPList) lookup(ip net.IP) (Range, bool) {
70 | var rng Range
71 | ok := false
72 | for _, r := range ipl.ranges {
73 | ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0
74 | if ok {
75 | rng = r
76 | break
77 | }
78 | }
79 | return rng, ok
80 | }
81 |
82 | func minifyIP(ip *net.IP) {
83 | v4 := ip.To4()
84 | if v4 != nil {
85 | *ip = append(make([]byte, 0, 4), v4...)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/server/web/pages/route.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "github.com/anacrolix/torrent/metainfo"
5 | "github.com/gin-gonic/gin"
6 |
7 | "server/settings"
8 | "server/torr"
9 | "server/web/auth"
10 | "server/web/pages/template"
11 |
12 | "golang.org/x/exp/slices"
13 | )
14 |
15 | func SetupRoute(route gin.IRouter) {
16 | authorized := route.Group("/", auth.CheckAuth())
17 |
18 | webPagesAuth := route.Group("/", func() gin.HandlerFunc {
19 | return func(c *gin.Context) {
20 | if slices.Contains([]string{"/site.webmanifest"}, c.FullPath()) {
21 | return
22 | }
23 | auth.CheckAuth()(c)
24 | }
25 | }())
26 |
27 | template.RouteWebPages(webPagesAuth)
28 | authorized.GET("/stat", statPage)
29 | authorized.GET("/magnets", getTorrents)
30 | }
31 |
32 | // stat godoc
33 | //
34 | // @Summary TorrServer Statistics
35 | // @Description Show server and torrents statistics.
36 | //
37 | // @Tags Pages
38 | //
39 | // @Produce text/plain
40 | // @Success 200 "TorrServer statistics"
41 | // @Router /stat [get]
42 | func statPage(c *gin.Context) {
43 | torr.WriteStatus(c.Writer)
44 | c.Status(200)
45 | }
46 |
47 | // getTorrents godoc
48 | //
49 | // @Summary Get HTML of magnet links
50 | // @Description Get HTML of magnet links.
51 | //
52 | // @Tags Pages
53 | //
54 | // @Produce text/html
55 | // @Success 200 "HTML with Magnet links"
56 | // @Router /magnets [get]
57 | func getTorrents(c *gin.Context) {
58 | list := settings.ListTorrent()
59 | http := "
{t('PWAGuide.Description')}
44 | 45 |{t('PWAGuide.VLC')}
46 | 47 |
48 | 1. {t('PWAGuide.FirstStep')}
52 | 2. {t('PWAGuide.SecondStep.Select')} {t('PWAGuide.SecondStep.AddToHomeScreen')} 53 |
54 |