├── .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 := "
" 60 | for _, db := range list { 61 | ts := db.TorrentSpec 62 | mi := metainfo.MetaInfo{ 63 | AnnounceList: ts.Trackers, 64 | } 65 | // mag := mi.Magnet(ts.DisplayName, ts.InfoHash) 66 | mag := mi.Magnet(&ts.InfoHash, &metainfo.Info{Name: ts.DisplayName}) 67 | http += "

magnet:?xt=urn:btih:" + mag.InfoHash.HexString() + "

" 68 | } 69 | http += "
" 70 | c.Data(200, "text/html; charset=utf-8", []byte(http)) 71 | } 72 | -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1136-640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1136-640.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1334-750.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1334-750.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-1792-828.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-1792-828.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2048-1536.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2048-1536.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2160-1620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2160-1620.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2208-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2208-1242.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2224-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2224-1668.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2388-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2388-1668.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2436-1125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2436-1125.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2532-1170.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2532-1170.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2688-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2688-1242.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2732-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2732-2048.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-2778-1284.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-2778-1284.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /server/web/pages/template/pages/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "./static/js/main.88d118ab.chunk.js", 4 | "main.js.map": "./static/js/main.88d118ab.chunk.js.map", 5 | "runtime-main.js": "./static/js/runtime-main.5ed86a79.js", 6 | "runtime-main.js.map": "./static/js/runtime-main.5ed86a79.js.map", 7 | "static/js/2.292d2615.chunk.js": "./static/js/2.292d2615.chunk.js", 8 | "static/js/2.292d2615.chunk.js.map": "./static/js/2.292d2615.chunk.js.map", 9 | "index.html": "./index.html", 10 | "static/js/2.292d2615.chunk.js.LICENSE.txt": "./static/js/2.292d2615.chunk.js.LICENSE.txt" 11 | }, 12 | "entrypoints": [ 13 | "static/js/runtime-main.5ed86a79.js", 14 | "static/js/2.292d2615.chunk.js", 15 | "static/js/main.88d118ab.chunk.js" 16 | ] 17 | } -------------------------------------------------------------------------------- /server/web/pages/template/pages/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00a572 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /server/web/pages/template/pages/dlnaicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/dlnaicon-120.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/dlnaicon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/dlnaicon-48.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/favicon-16x16.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/favicon-32x32.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/favicon.ico -------------------------------------------------------------------------------- /server/web/pages/template/pages/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/icon.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/logo.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/server/web/pages/template/pages/mstile-150x150.png -------------------------------------------------------------------------------- /server/web/pages/template/pages/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TorrServer", 3 | "short_name": "TorrServer", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any" 10 | }, 11 | { 12 | "src": "logo.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /server/web/pages/template/pages/static/js/2.292d2615.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * The buffer module from node.js, for the browser. 9 | * 10 | * @author Feross Aboukhadijeh 11 | * @license MIT 12 | */ 13 | 14 | /*! blob-to-buffer. MIT License. Feross Aboukhadijeh */ 15 | 16 | /*! https://mths.be/punycode v1.4.1 by @mathias */ 17 | 18 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 19 | 20 | /*! magnet-uri. MIT License. WebTorrent LLC */ 21 | 22 | /*! parse-torrent. MIT License. WebTorrent LLC */ 23 | 24 | /*! queue-microtask. MIT License. Feross Aboukhadijeh */ 25 | 26 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 27 | 28 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 29 | 30 | /*! simple-concat. MIT License. Feross Aboukhadijeh */ 31 | 32 | /*! simple-get. MIT License. Feross Aboukhadijeh */ 33 | 34 | /** 35 | * A better abstraction over CSS. 36 | * 37 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 38 | * @website https://github.com/cssinjs/jss 39 | * @license MIT 40 | */ 41 | 42 | /** @license React v0.20.2 43 | * scheduler.production.min.js 44 | * 45 | * Copyright (c) Facebook, Inc. and its affiliates. 46 | * 47 | * This source code is licensed under the MIT license found in the 48 | * LICENSE file in the root directory of this source tree. 49 | */ 50 | 51 | /** @license React v16.13.1 52 | * react-is.production.min.js 53 | * 54 | * Copyright (c) Facebook, Inc. and its affiliates. 55 | * 56 | * This source code is licensed under the MIT license found in the 57 | * LICENSE file in the root directory of this source tree. 58 | */ 59 | 60 | /** @license React v17.0.2 61 | * react-dom.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** @license React v17.0.2 70 | * react-is.production.min.js 71 | * 72 | * Copyright (c) Facebook, Inc. and its affiliates. 73 | * 74 | * This source code is licensed under the MIT license found in the 75 | * LICENSE file in the root directory of this source tree. 76 | */ 77 | 78 | /** @license React v17.0.2 79 | * react-jsx-runtime.production.min.js 80 | * 81 | * Copyright (c) Facebook, Inc. and its affiliates. 82 | * 83 | * This source code is licensed under the MIT license found in the 84 | * LICENSE file in the root directory of this source tree. 85 | */ 86 | 87 | /** @license React v17.0.2 88 | * react.production.min.js 89 | * 90 | * Copyright (c) Facebook, Inc. and its affiliates. 91 | * 92 | * This source code is licensed under the MIT license found in the 93 | * LICENSE file in the root directory of this source tree. 94 | */ 95 | -------------------------------------------------------------------------------- /server/web/pages/template/pages/static/js/runtime-main.5ed86a79.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c `http://192.168.78.4:8090` - correct 9 | > 10 | > `http://192.168.78.4:8090/` - wrong 11 | 3. in `.env` file add TMDB api key 12 | 4. `NODE_OPTIONS=--openssl-legacy-provider yarn start` 13 | 14 | ### Eslint 15 | > Prettier will fix the code every time the code is saved 16 | 17 | - `yarn lint` - to find all linting problems 18 | - `yarn fix` - to fix code 19 | 20 | ### How images were generated 21 | `npx pwa-asset-generator public/logo.png public -m public/site.webmanifest -p "calc(50vh - 25%) calc(50vw - 25%)" -b "linear-gradient(135deg, rgb(50,54,55), rgb(84,90,94))" -q 100 -i public/index.html -f` -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torrserver_web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.4", 7 | "@material-ui/icons": "^4.11.2", 8 | "axios": "^1.8.2", 9 | "clsx": "^1.1.1", 10 | "i18next": "^20.3.1", 11 | "i18next-browser-languagedetector": "^6.1.8", 12 | "lodash": "^4.17.21", 13 | "material-ui-image": "^3.3.2", 14 | "parse-torrent": "^9.1.3", 15 | "parse-torrent-title": "^1.3.0", 16 | "polished": "^4.1.3", 17 | "react": "^17.0.2", 18 | "react-copy-to-clipboard": "^5.0.3", 19 | "react-div-100vh": "^0.6.0", 20 | "react-dom": "^17.0.2", 21 | "react-dropzone": "^11.3.2", 22 | "react-i18next": "^11.10.0", 23 | "react-measure": "^2.5.2", 24 | "react-query": "^3.39.3", 25 | "react-scripts": "4.0.3", 26 | "react-swipeable-views": "^0.14.0", 27 | "styled-components": "^5.3.11", 28 | "uuid": "^8.3.2" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject", 35 | "lint": "eslint --ext .js,.jsx src --color", 36 | "fix": "yarn lint --fix" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.23.0", 52 | "@babel/core": "^7.23.3", 53 | "@babel/plugin-proposal-class-properties": "^7.13.0", 54 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 55 | "@babel/plugin-transform-react-jsx": "^7.22.15", 56 | "babel-minify": "^0.5.1", 57 | "babel-preset-minify": "^0.5.1", 58 | "eslint": "^7.27.0", 59 | "eslint-config-airbnb": "^18.2.1", 60 | "eslint-config-prettier": "^8.10.0", 61 | "eslint-plugin-prettier": "^3.4.0", 62 | "prettier": "^2.8.8" 63 | }, 64 | "description": "", 65 | "main": "gulpfile.js", 66 | "keywords": [], 67 | "author": "", 68 | "license": "ISC", 69 | "homepage": "./" 70 | } 71 | -------------------------------------------------------------------------------- /web/public/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1136-640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1136-640.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1334-750.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1334-750.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-1792-828.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-1792-828.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2048-1536.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2048-1536.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2160-1620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2160-1620.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2208-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2208-1242.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2224-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2224-1668.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2388-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2388-1668.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2436-1125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2436-1125.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2532-1170.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2532-1170.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2688-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2688-1242.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2732-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2732-2048.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-2778-1284.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-2778-1284.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /web/public/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /web/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00a572 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/public/dlnaicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/dlnaicon-120.png -------------------------------------------------------------------------------- /web/public/dlnaicon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/dlnaicon-48.png -------------------------------------------------------------------------------- /web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/icon.png -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/logo.png -------------------------------------------------------------------------------- /web/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouROK/TorrServer/b556ecbce721761fb0164ffd69666eb31ce7ac01/web/public/mstile-150x150.png -------------------------------------------------------------------------------- /web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TorrServer", 3 | "short_name": "TorrServer", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any" 10 | }, 11 | { 12 | "src": "logo.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /web/src/components/About/LinkComponent.jsx: -------------------------------------------------------------------------------- 1 | import { GitHub as GitHubIcon } from '@material-ui/icons' 2 | 3 | import { LinkWrapper, LinkIcon } from './style' 4 | 5 | export default function LinkComponent({ name, link }) { 6 | return ( 7 | 8 | {link && ( 9 | 10 | 11 | 12 | )} 13 | 14 |
{name}
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/components/About/style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import { standaloneMedia } from 'style/standaloneMedia' 3 | 4 | export const DialogWrapper = styled.div` 5 | height: 100%; 6 | display: grid; 7 | grid-template-rows: max-content 1fr max-content; 8 | ` 9 | 10 | export const HeaderSection = styled.section` 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | font-size: 36px; 15 | font-weight: 300; 16 | padding: 20px; 17 | 18 | img { 19 | width: 64px; 20 | } 21 | 22 | @media (max-width: 930px) { 23 | font-size: 22px; 24 | padding: 10px 20px; 25 | 26 | img { 27 | width: 60px; 28 | } 29 | } 30 | 31 | ${standaloneMedia(css` 32 | padding-top: 30px; 33 | `)} 34 | ` 35 | 36 | export const ThanksSection = styled.section` 37 | padding: 20px; 38 | text-align: center; 39 | font-size: 24px; 40 | font-weight: 300; 41 | background: #e8e5eb; 42 | color: #323637; 43 | 44 | @media (max-width: 930px) { 45 | font-size: 20px; 46 | padding: 30px 20px; 47 | } 48 | ` 49 | 50 | export const Section = styled.section` 51 | padding: 20px; 52 | 53 | > span { 54 | font-size: 22px; 55 | display: block; 56 | margin-bottom: 15px; 57 | } 58 | 59 | a { 60 | text-decoration: none; 61 | } 62 | 63 | > div { 64 | display: grid; 65 | gap: 10px; 66 | grid-template-columns: repeat(4, max-content); 67 | 68 | @media (max-width: 930px) { 69 | grid-template-columns: repeat(3, 1fr); 70 | } 71 | 72 | @media (max-width: 780px) { 73 | grid-template-columns: repeat(2, 1fr); 74 | } 75 | 76 | @media (max-width: 550px) { 77 | grid-template-columns: 1fr; 78 | } 79 | } 80 | ` 81 | 82 | export const FooterSection = styled.div` 83 | padding: 20px; 84 | display: flex; 85 | justify-content: flex-end; 86 | background: #e8e5eb; 87 | ` 88 | 89 | export const LinkWrapper = styled.a` 90 | ${({ isLink }) => css` 91 | display: inline-flex; 92 | align-items: center; 93 | justify-content: start; 94 | border: 1px solid; 95 | padding: 7px 10px; 96 | border-radius: 5px; 97 | text-transform: uppercase; 98 | text-decoration: none; 99 | background: #545a5e; 100 | color: #f1eff3; 101 | transition: 0.2s; 102 | 103 | > * { 104 | transition: 0.2s; 105 | } 106 | 107 | ${isLink 108 | ? css` 109 | :hover { 110 | filter: brightness(1.1); 111 | 112 | > * { 113 | transform: translateY(0px); 114 | } 115 | } 116 | ` 117 | : css` 118 | cursor: default; 119 | `} 120 | `} 121 | ` 122 | 123 | export const LinkIcon = styled.div` 124 | display: grid; 125 | margin-right: 10px; 126 | ` 127 | -------------------------------------------------------------------------------- /web/src/components/Add/LeftSideComponent.jsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | import { useDropzone } from 'react-dropzone' 3 | import { AddItemIcon, TorrentIcon } from 'icons' 4 | import TextField from '@material-ui/core/TextField' 5 | import { Cancel as CancelIcon } from '@material-ui/icons' 6 | import { useState } from 'react' 7 | 8 | import { 9 | CancelIconWrapper, 10 | IconWrapper, 11 | LeftSide, 12 | LeftSideBottomSectionFileSelected, 13 | LeftSideBottomSectionNoFile, 14 | LeftSideTopSection, 15 | TorrentIconWrapper, 16 | } from './style' 17 | 18 | export default function LeftSideComponent({ 19 | setIsUserInteractedWithPoster, 20 | setSelectedFile, 21 | torrentSource, 22 | setTorrentSource, 23 | selectedFile, 24 | }) { 25 | const { t } = useTranslation() 26 | 27 | const handleCapture = files => { 28 | const [file] = files 29 | if (!file) return 30 | 31 | setIsUserInteractedWithPoster(false) 32 | setSelectedFile(file) 33 | setTorrentSource(file.name) 34 | } 35 | 36 | const clearSelectedFile = () => { 37 | setSelectedFile() 38 | setTorrentSource('') 39 | } 40 | 41 | const [isTorrentSourceActive, setIsTorrentSourceActive] = useState(false) 42 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 43 | onDrop: handleCapture, 44 | accept: '.torrent', 45 | multiple: false, 46 | }) 47 | 48 | const handleTorrentSourceChange = ({ target: { value } }) => setTorrentSource(value) 49 | 50 | return ( 51 | 52 | 53 | setIsTorrentSourceActive(true)} 64 | onBlur={() => setIsTorrentSourceActive(false)} 65 | inputProps={{ autoComplete: 'off' }} 66 | disabled={!!selectedFile} 67 | /> 68 | 69 | 70 | {selectedFile ? ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ) : ( 81 | 82 | 83 |
{t('AddDialog.AppendFile.Or')}
84 | 85 | 86 | 87 |
{t('AddDialog.AppendFile.ClickOrDrag')}
88 |
89 |
90 | )} 91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /web/src/components/Add/helpers.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import parseTorrent from 'parse-torrent' 3 | import ptt from 'parse-torrent-title' 4 | 5 | export const getMoviePosters = (movieName, language = 'en') => { 6 | const url = `${window.location.protocol}//api.themoviedb.org/3/search/multi` 7 | const imgHost = `${window.location.protocol}//${language === 'ru' ? 'imagetmdb.com' : 'image.tmdb.org'}` 8 | 9 | return axios 10 | .get(url, { 11 | params: { 12 | api_key: process.env.REACT_APP_TMDB_API_KEY, 13 | language, 14 | include_image_language: `${language},null,en`, 15 | query: movieName, 16 | }, 17 | }) 18 | .then(({ data: { results } }) => 19 | results.filter(el => el.poster_path).map(el => `${imgHost}/t/p/w300${el.poster_path}`), 20 | ) 21 | .catch(() => null) 22 | } 23 | 24 | export const checkImageURL = async url => { 25 | if (!url || !url.match(/.(\.jpg|\.jpeg|\.png|\.gif|\.svg||\.webp).*$/i)) return false 26 | return true 27 | } 28 | 29 | const magnetRegex = /^magnet:\?xt=urn:[a-z0-9].*/i 30 | export const hashRegex = /^\b[0-9a-f]{32}\b$|^\b[0-9a-f]{40}\b$|^\b[0-9a-f]{64}\b$/i 31 | const torrentRegex = /^.*\.(torrent)$/i 32 | const linkRegex = /^(http(s?)):\/\/.*/i 33 | 34 | export const checkTorrentSource = source => 35 | source.match(hashRegex) !== null || 36 | source.match(magnetRegex) !== null || 37 | source.match(torrentRegex) !== null || 38 | source.match(linkRegex) !== null 39 | 40 | export const parseTorrentTitle = (parsingSource, callback) => { 41 | parseTorrent.remote(parsingSource, (err, { name, files } = {}) => { 42 | if (!name || err) return callback({ parsedTitle: null, originalName: null }) 43 | 44 | const torrentName = ptt.parse(name).title 45 | const nameOfFileInsideTorrent = files ? ptt.parse(files[0].name).title : null 46 | 47 | let newTitle = torrentName 48 | if (nameOfFileInsideTorrent) { 49 | // taking shorter title because in most cases it is more accurate 50 | newTitle = torrentName.length < nameOfFileInsideTorrent.length ? torrentName : nameOfFileInsideTorrent 51 | } 52 | 53 | callback({ parsedTitle: newTitle, originalName: name }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /web/src/components/Add/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import ListItemIcon from '@material-ui/core/ListItemIcon' 3 | import LibraryAddIcon from '@material-ui/icons/LibraryAdd' 4 | import ListItemText from '@material-ui/core/ListItemText' 5 | import { useTranslation } from 'react-i18next' 6 | import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' 7 | import { isStandaloneApp } from 'utils/Utils' 8 | 9 | import AddDialog from './AddDialog' 10 | import { StyledPWAAddButton } from './style' 11 | 12 | export default function AddDialogButton({ isOffline, isLoading }) { 13 | const { t } = useTranslation() 14 | const [isDialogOpen, setIsDialogOpen] = useState(false) 15 | const handleClickOpen = () => setIsDialogOpen(true) 16 | const handleClose = () => setIsDialogOpen(false) 17 | 18 | return ( 19 |
20 | 21 | {isStandaloneApp ? ( 22 | 23 | ) : ( 24 | <> 25 | 26 | 27 | 28 | 29 | 30 | 31 | )} 32 | 33 | 34 | {isDialogOpen && } 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /web/src/components/App/PWAFooter/index.jsx: -------------------------------------------------------------------------------- 1 | import { CreditCard as CreditCardIcon } from '@material-ui/icons' 2 | import { useTranslation } from 'react-i18next' 3 | import CloseServer from 'components/CloseServer' 4 | import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' 5 | import AddDialogButton from 'components/Add' 6 | import AboutDialog from 'components/About' 7 | import SettingsDialogButton from 'components/Settings' 8 | 9 | import StyledPWAFooter from './style' 10 | 11 | export default function PWAFooter({ setIsDonationDialogOpen, isOffline, isLoading }) { 12 | const { t } = useTranslation() 13 | 14 | return ( 15 | 16 | 17 | 18 | setIsDonationDialogOpen(true)}> 19 | 20 | 21 |
{t('Donate')}
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /web/src/components/App/PWAFooter/style.js: -------------------------------------------------------------------------------- 1 | import { standaloneMedia } from 'style/standaloneMedia' 2 | import styled, { css } from 'styled-components' 3 | 4 | export const pwaFooterHeight = 90 5 | 6 | export default styled.div` 7 | background: #575757; 8 | color: #fff; 9 | position: fixed; 10 | bottom: 0; 11 | width: 100%; 12 | height: ${pwaFooterHeight}px; 13 | 14 | display: none; 15 | 16 | ${standaloneMedia(css` 17 | display: grid; 18 | grid-template-columns: repeat(5, calc(100% / 5)); 19 | justify-items: center; 20 | `)} 21 | ` 22 | -------------------------------------------------------------------------------- /web/src/components/App/PWAInstallationGuide/IOSShareIcon.jsx: -------------------------------------------------------------------------------- 1 | export default function IOSShareIcon() { 2 | return ( 3 | 15 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /web/src/components/App/PWAInstallationGuide/index.jsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton' 2 | import CloseIcon from '@material-ui/icons/Close' 3 | import { useState } from 'react' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | import IOSShareIcon from './IOSShareIcon' 7 | import { StyledWrapper, StyledHeader, StyledContent } from './style' 8 | 9 | export function PWAInstallationGuide() { 10 | const pwaNotificationIsClosed = JSON.parse(localStorage.getItem('pwaNotificationIsClosed')) 11 | const [isOpen, setIsOpen] = useState(!pwaNotificationIsClosed) 12 | const [shouldBeOpened, setShouldBeOpened] = useState(!pwaNotificationIsClosed) 13 | 14 | const { t } = useTranslation() 15 | 16 | if (!isOpen) return null 17 | 18 | return ( 19 | 20 | 21 | ts-icon 22 | 23 | {t('PWAGuide.Header')} 24 | 25 | { 30 | setShouldBeOpened(false) 31 | 32 | setTimeout(() => { 33 | setIsOpen(false) 34 | localStorage.setItem('pwaNotificationIsClosed', true) 35 | }, 300) 36 | }} 37 | > 38 | 39 | 40 | 41 | 42 | 43 |

{t('PWAGuide.Description')}

44 | 45 |

{t('PWAGuide.VLC')}

46 | 47 |

48 | 1. {t('PWAGuide.FirstStep')} 49 |

50 | 51 |

52 | 2. {t('PWAGuide.SecondStep.Select')} {t('PWAGuide.SecondStep.AddToHomeScreen')} 53 |

54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /web/src/components/App/PWAInstallationGuide/style.jsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | export const StyledWrapper = styled.div` 4 | ${({ isOpen }) => css` 5 | position: absolute; 6 | bottom: 10px; 7 | left: 50%; 8 | background: #eeeef0; 9 | width: calc(100% - 20px); 10 | z-index: 9999; 11 | border-radius: 10px; 12 | transition: all 0.3s; 13 | color: #000; 14 | 15 | ${isOpen 16 | ? css` 17 | opacity: 1; 18 | transform: translate(-50%, 0); 19 | ` 20 | : css` 21 | transform: translate(-50%, 150%); 22 | opacity: 0; 23 | pointer-events: none; 24 | `} 25 | 26 | > :not(:last-child) { 27 | border-bottom: 1px solid #dadadc; 28 | } 29 | 30 | > * { 31 | padding: 20px; 32 | } 33 | `} 34 | ` 35 | 36 | export const StyledHeader = styled.div` 37 | display: grid; 38 | grid-auto-flow: column; 39 | grid-template-columns: min-content 1fr; 40 | gap: 20px; 41 | align-items: center; 42 | font-weight: 700; 43 | 44 | img { 45 | border-radius: 5px; 46 | } 47 | ` 48 | 49 | export const StyledContent = styled.div` 50 | > :not(:last-child) { 51 | margin-bottom: 25px; 52 | } 53 | 54 | span { 55 | background: #fefcfd; 56 | padding: 5px; 57 | border-radius: 5px; 58 | } 59 | ` 60 | -------------------------------------------------------------------------------- /web/src/components/App/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import Divider from '@material-ui/core/Divider' 2 | import ListItem from '@material-ui/core/ListItem' 3 | import ListItemIcon from '@material-ui/core/ListItemIcon' 4 | import ListItemText from '@material-ui/core/ListItemText' 5 | import { CreditCard as CreditCardIcon } from '@material-ui/icons' 6 | import List from '@material-ui/core/List' 7 | import { useTranslation } from 'react-i18next' 8 | import AddDialogButton from 'components/Add' 9 | import SettingsDialog from 'components/Settings' 10 | import RemoveAll from 'components/RemoveAll' 11 | import AboutDialog from 'components/About' 12 | import CloseServer from 'components/CloseServer' 13 | import { memo } from 'react' 14 | import CheckIcon from '@material-ui/icons/Check' 15 | import ClearIcon from '@material-ui/icons/Clear' 16 | import { TORRENT_CATEGORIES } from 'components/categories' 17 | import FilterByCategory from 'components/FilterByCategory' 18 | 19 | import { AppSidebarStyle } from './style' 20 | 21 | const Sidebar = ({ isDrawerOpen, setIsDonationDialogOpen, isOffline, isLoading, setGlobalFilterCategory }) => { 22 | const { t } = useTranslation() 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 40 | setGlobalFilterCategory={setGlobalFilterCategory} 41 | /> 42 | {TORRENT_CATEGORIES.map(category => ( 43 | 50 | ))} 51 | } 56 | setGlobalFilterCategory={setGlobalFilterCategory} 57 | /> 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | setIsDonationDialogOpen(true)}> 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | } 80 | 81 | export default memo(Sidebar) 82 | -------------------------------------------------------------------------------- /web/src/components/CloseServer.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Button, DialogActions, DialogTitle, ListItemIcon, ListItemText } from '@material-ui/core' 3 | import { StyledDialog, StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' 4 | import { PowerSettingsNew as PowerSettingsNewIcon, PowerOff as PowerOffIcon } from '@material-ui/icons' 5 | import { shutdownHost } from 'utils/Hosts' 6 | import { useTranslation } from 'react-i18next' 7 | import { isStandaloneApp } from 'utils/Utils' 8 | import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' 9 | 10 | import UnsafeButton from './UnsafeButton' 11 | 12 | export default function CloseServer({ isOffline, isLoading }) { 13 | const { t } = useTranslation() 14 | const [open, setOpen] = useState(false) 15 | const closeDialog = () => setOpen(false) 16 | const openDialog = () => setOpen(true) 17 | 18 | const ref = useOnStandaloneAppOutsideClick(closeDialog) 19 | 20 | return ( 21 | <> 22 | 23 | {isStandaloneApp ? ( 24 | <> 25 | 26 |
{t('TurnOff')}
27 | 28 | ) : ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | 36 | )} 37 |
38 | 39 | 40 | {t('CloseServer?')} 41 | 42 | 45 | 46 | } 49 | variant='contained' 50 | onClick={() => { 51 | fetch(shutdownHost()) 52 | closeDialog() 53 | }} 54 | color='secondary' 55 | autoFocus 56 | > 57 | {t('TurnOff')} 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | import { Checkbox, FormControlLabel } from '@material-ui/core' 3 | import { useState } from 'react' 4 | 5 | import { SectionTitle, WidgetWrapper } from '../style' 6 | import { DetailedViewCacheSection, DetailedViewWidgetSection } from './style' 7 | import TorrentCache from '../TorrentCache' 8 | import { 9 | SizeWidget, 10 | PiecesLengthWidget, 11 | StatusWidget, 12 | PiecesCountWidget, 13 | PeersWidget, 14 | UploadSpeedWidget, 15 | DownlodSpeedWidget, 16 | } from '../widgets' 17 | 18 | export default function DetailedView({ 19 | downloadSpeed, 20 | uploadSpeed, 21 | torrent, 22 | torrentSize, 23 | PiecesCount, 24 | PiecesLength, 25 | stat, 26 | cache, 27 | }) { 28 | const { t } = useTranslation() 29 | const [isSnakeDebugMode, setIsSnakeDebugMode] = useState( 30 | JSON.parse(localStorage.getItem('isSnakeDebugMode')) || false, 31 | ) 32 | 33 | return ( 34 | <> 35 | 36 | {t('Data')} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | {t('Cache')} 53 | 54 | { 61 | setIsSnakeDebugMode(checked) 62 | localStorage.setItem('isSnakeDebugMode', checked) 63 | }} 64 | /> 65 | } 66 | label={t('DebugMode')} 67 | labelPlacement='start' 68 | /> 69 |
70 |
71 | 72 | 73 |
74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/DetailedView/style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | export const DetailedViewWidgetSection = styled.section` 4 | ${({ 5 | theme: { 6 | detailedView: { gradientStartColor, gradientEndColor }, 7 | }, 8 | }) => css` 9 | padding: 40px; 10 | background: linear-gradient(145deg, ${gradientStartColor}, ${gradientEndColor}); 11 | 12 | @media (max-width: 800px) { 13 | padding: 20px; 14 | } 15 | `} 16 | ` 17 | 18 | export const DetailedViewCacheSection = styled.section` 19 | ${({ 20 | theme: { 21 | detailedView: { cacheSectionBGColor }, 22 | }, 23 | }) => css` 24 | padding: 40px; 25 | box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5); 26 | background: ${cacheSectionBGColor}; 27 | flex: 1; 28 | 29 | @media (max-width: 800px) { 30 | padding: 20px; 31 | } 32 | `} 33 | ` 34 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx: -------------------------------------------------------------------------------- 1 | import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core' 2 | import CloseIcon from '@material-ui/icons/Close' 3 | import { ArrowBack } from '@material-ui/icons' 4 | import { isStandaloneApp } from 'utils/Utils' 5 | 6 | const useStyles = makeStyles({ 7 | appBar: { position: 'relative', ...(isStandaloneApp && { paddingTop: '30px' }) }, 8 | title: { marginLeft: '5px', flex: 1 }, 9 | }) 10 | 11 | export default function DialogHeader({ title, onClose, onBack }) { 12 | const classes = useStyles() 13 | 14 | return ( 15 | 16 | 17 | {onBack && ( 18 | 19 | 20 | 21 | )} 22 | 23 | 24 | {title} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/StatisticsField.jsx: -------------------------------------------------------------------------------- 1 | import { WidgetFieldWrapper, WidgetFieldIcon, WidgetFieldValue, WidgetFieldTitle } from './style' 2 | 3 | export default function StatisticsField({ icon: Icon, title, value, iconBg, valueBg }) { 4 | return ( 5 | 6 | {title} 7 | 8 | 9 | 10 | 11 | {value} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js: -------------------------------------------------------------------------------- 1 | export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => { 2 | const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ percentage }) => percentage > 0) 3 | 4 | const getFullAmountOfBlocks = amountOfBlocks => 5 | // this function counts existed amount of blocks with extra "empty blocks" to fill the row till the end 6 | amountOfBlocks % piecesInOneRow === 0 7 | ? amountOfBlocks - 1 8 | : amountOfBlocks + piecesInOneRow - (amountOfBlocks % piecesInOneRow) - 1 || 0 9 | 10 | const amountOfBlocksToRenderInShortView = getFullAmountOfBlocks(preloadPiecesAmount) 11 | // preloadPiecesAmount is counted from "cache.Capacity / cache.PiecesLength". We always show at least this amount of blocks 12 | const scalableAmountOfBlocksToRenderInShortView = getFullAmountOfBlocks(cacheMapWithoutEmptyBlocks.length) 13 | // cacheMap can become bigger than preloadPiecesAmount counted before. In that case we count blocks dynamically 14 | 15 | const finalAmountOfBlocksToRenderInShortView = Math.max( 16 | // this check is needed to decide which is the biggest amount of blocks and take it to render 17 | scalableAmountOfBlocksToRenderInShortView, 18 | amountOfBlocksToRenderInShortView, 19 | ) 20 | 21 | const extraBlocksAmount = finalAmountOfBlocksToRenderInShortView - cacheMapWithoutEmptyBlocks.length + 1 22 | // amount of blocks needed to fill the line till the end 23 | 24 | const extraEmptyBlocksForFillingLine = extraBlocksAmount ? new Array(extraBlocksAmount).fill({}) : [] 25 | 26 | return [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine] 27 | } 28 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js: -------------------------------------------------------------------------------- 1 | import { rgba } from 'polished' 2 | import { mainColors } from 'style/colors' 3 | 4 | export const snakeSettings = { 5 | dark: { 6 | default: { 7 | borderWidth: 1, 8 | pieceSize: 14, 9 | gapBetweenPieces: 3, 10 | borderColor: rgba('#fff', 0.2), 11 | completeColor: rgba(mainColors.dark.primary, 0.5), 12 | backgroundColor: '#949ca0', 13 | progressColor: rgba('#fff', 0.2), 14 | readerColor: '#8f0405', 15 | rangeColor: '#cda184', 16 | }, 17 | mini: { 18 | cacheMaxHeight: 340, 19 | borderWidth: 2, 20 | pieceSize: 23, 21 | gapBetweenPieces: 6, 22 | borderColor: '#5c6469', 23 | completeColor: '#5c6469', 24 | backgroundColor: '#949ca0', 25 | progressColor: '#949ca0', 26 | readerColor: '#ccc', 27 | rangeColor: '#cda184', 28 | }, 29 | }, 30 | light: { 31 | default: { 32 | borderWidth: 1, 33 | pieceSize: 14, 34 | gapBetweenPieces: 3, 35 | borderColor: '#dbf2e8', 36 | completeColor: mainColors.light.primary, 37 | backgroundColor: '#fff', 38 | progressColor: '#b3dfc9', 39 | readerColor: '#000', 40 | rangeColor: '#afa6e3', 41 | }, 42 | mini: { 43 | cacheMaxHeight: 340, 44 | borderWidth: 2, 45 | pieceSize: 23, 46 | gapBetweenPieces: 6, 47 | borderColor: '#4db380', 48 | completeColor: '#4db380', 49 | backgroundColor: '#dbf2e8', 50 | progressColor: '#dbf2e8', 51 | readerColor: '#0a0a0a', 52 | rangeColor: '#afa6e3', 53 | }, 54 | }, 55 | } 56 | 57 | export const createGradient = (ctx, percentage, theme, snakeType) => { 58 | const { pieceSize, completeColor, progressColor } = snakeSettings[theme][snakeType] 59 | 60 | const gradient = ctx.createLinearGradient(0, pieceSize, 0, 0) 61 | gradient.addColorStop(0, completeColor) 62 | gradient.addColorStop(percentage / 100, completeColor) 63 | gradient.addColorStop(percentage / 100, progressColor) 64 | gradient.addColorStop(1, progressColor) 65 | 66 | return gradient 67 | } 68 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | import { snakeSettings } from './snakeSettings' 4 | 5 | export const ScrollNotification = styled.div` 6 | margin-top: 10px; 7 | text-transform: uppercase; 8 | color: rgba(0, 0, 0, 0.5); 9 | align-self: center; 10 | ` 11 | 12 | export const SnakeWrapper = styled.div` 13 | ${({ isMini, themeType }) => css` 14 | ${isMini && 15 | css` 16 | display: grid; 17 | justify-content: center; 18 | max-height: ${snakeSettings[themeType].mini.cacheMaxHeight}px; 19 | overflow: auto; 20 | `} 21 | 22 | canvas { 23 | display: block; 24 | } 25 | `} 26 | ` 27 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/TorrentFunctions/style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | export const MainSectionButtonGroup = styled.div` 4 | display: grid; 5 | grid-template-columns: repeat(3, 1fr); 6 | gap: 20px; 7 | 8 | :not(:last-child) { 9 | margin-bottom: 30px; 10 | } 11 | 12 | @media (max-width: 1580px) { 13 | grid-template-columns: repeat(2, 1fr); 14 | } 15 | 16 | @media (max-width: 880px) { 17 | grid-template-columns: 1fr; 18 | } 19 | ` 20 | 21 | export const SmallLabel = styled.div` 22 | ${({ 23 | mb, 24 | theme: { 25 | torrentFunctions: { fontColor }, 26 | }, 27 | }) => css` 28 | ${mb && `margin-bottom: ${mb}px`}; 29 | font-size: 20px; 30 | font-weight: 300; 31 | line-height: 1; 32 | color: ${fontColor}; 33 | 34 | @media (max-width: 800px) { 35 | font-size: 18px; 36 | ${mb && `margin-bottom: ${mb / 1.5}px`}; 37 | } 38 | `} 39 | ` 40 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/customHooks.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import { cacheHost, settingsHost } from 'utils/Hosts' 3 | import axios from 'axios' 4 | 5 | export const useUpdateCache = hash => { 6 | const [cache, setCache] = useState({}) 7 | const componentIsMounted = useRef(true) 8 | const timerID = useRef(null) 9 | 10 | useEffect( 11 | () => () => { 12 | // this function is required to notify "updateCache" when NOT to make state update 13 | componentIsMounted.current = false 14 | }, 15 | [], 16 | ) 17 | 18 | useEffect(() => { 19 | if (hash) { 20 | timerID.current = setInterval(() => { 21 | const updateCache = newCache => componentIsMounted.current && setCache(newCache) 22 | 23 | axios 24 | .post(cacheHost(), { action: 'get', hash }) 25 | .then(({ data }) => updateCache(data)) 26 | // empty cache if error 27 | .catch(() => updateCache({})) 28 | }, 100) 29 | } else clearInterval(timerID.current) 30 | 31 | return () => clearInterval(timerID.current) 32 | }, [hash]) 33 | 34 | return cache 35 | } 36 | 37 | export const useCreateCacheMap = cache => { 38 | const [cacheMap, setCacheMap] = useState([]) 39 | 40 | useEffect(() => { 41 | const { PiecesCount, Pieces, Readers } = cache 42 | 43 | const map = [] 44 | 45 | for (let i = 0; i < PiecesCount; i++) { 46 | const { Size, Length, Priority } = Pieces[i] || {} 47 | 48 | const newPiece = { id: i, percentage: (Size / Length) * 100 || 0, priority: Priority || 0 } 49 | 50 | Readers.forEach(r => { 51 | if (i === r.Reader) newPiece.isReader = true 52 | if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true 53 | }) 54 | 55 | map.push(newPiece) 56 | } 57 | setCacheMap(map) 58 | }, [cache]) 59 | 60 | return cacheMap 61 | } 62 | 63 | export const useGetSettings = cache => { 64 | const [settings, setSettings] = useState() 65 | useEffect(() => { 66 | axios.post(settingsHost(), { action: 'get' }).then(({ data }) => setSettings(data)) 67 | }, [cache]) 68 | 69 | return settings 70 | } 71 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/helpers.js: -------------------------------------------------------------------------------- 1 | const getExt = filename => { 2 | const ext = filename.split('.').pop() 3 | if (ext === filename) return '' 4 | return ext.toLowerCase() 5 | } 6 | const playableExtList = [ 7 | // video 8 | '3g2', 9 | '3gp', 10 | 'aaf', 11 | 'asf', 12 | 'avchd', 13 | 'avi', 14 | 'drc', 15 | 'dv', 16 | 'flv', 17 | 'iso', 18 | 'm2v', 19 | 'm2ts', 20 | 'm4p', 21 | 'm4v', 22 | 'mkv', 23 | 'mng', 24 | 'mov', 25 | 'mp2', 26 | 'mp4', 27 | 'mpe', 28 | 'mpeg', 29 | 'mpg', 30 | 'mpv', 31 | 'mts', 32 | 'mxf', 33 | 'nsv', 34 | 'ogv', 35 | 'ts', 36 | 'qt', 37 | 'rm', 38 | 'rmvb', 39 | 'roq', 40 | 'svi', 41 | 'vob', 42 | 'webm', 43 | 'wmv', 44 | 'yuv', 45 | // audio 46 | 'aac', 47 | 'aiff', 48 | 'ape', 49 | 'au', 50 | 'dsd', 51 | 'dff', 52 | 'dsf', 53 | 'flac', 54 | 'gsm', 55 | 'it', 56 | 'm3u', 57 | 'm4a', 58 | 'mid', 59 | 'mod', 60 | 'mp3', 61 | 'mpa', 62 | 'oga', 63 | 'ogg', 64 | 'opus', 65 | 'pls', 66 | 'ra', 67 | 's3m', 68 | 'sid', 69 | 'wav', 70 | 'weba', 71 | 'wma', 72 | 'xm', 73 | ] 74 | 75 | // eslint-disable-next-line import/prefer-default-export 76 | export const isFilePlayable = fileName => playableExtList.includes(getExt(fileName)) 77 | -------------------------------------------------------------------------------- /web/src/components/DialogTorrentDetailsContent/widgets/useGetWidgetColors.jsx: -------------------------------------------------------------------------------- 1 | import { DarkModeContext } from 'components/App' 2 | import { useContext } from 'react' 3 | import { THEME_MODES } from 'style/materialUISetup' 4 | 5 | const { LIGHT, DARK } = THEME_MODES 6 | 7 | const colors = { 8 | light: { 9 | downloadSpeed: { iconBGColor: '#118f00', valueBGColor: '#13a300' }, 10 | uploadSpeed: { iconBGColor: '#0146ad', valueBGColor: '#0058db' }, 11 | peers: { iconBGColor: '#cdc118', valueBGColor: '#d8cb18' }, 12 | piecesCount: { iconBGColor: '#b6c95e', valueBGColor: '#c0d076' }, 13 | piecesLength: { iconBGColor: '#0982c8', valueBGColor: '#098cd7' }, 14 | status: { iconBGColor: '#aea25b', valueBGColor: '#b4aa6e' }, 15 | size: { iconBGColor: '#9b01ad', valueBGColor: '#ac03bf' }, 16 | category: { iconBGColor: '#914820', valueBGColor: '#c9632c' }, 17 | }, 18 | dark: { 19 | downloadSpeed: { iconBGColor: '#0c6600', valueBGColor: '#0d7000' }, 20 | uploadSpeed: { iconBGColor: '#003f9e', valueBGColor: '#0047b3' }, 21 | peers: { iconBGColor: '#a69c11', valueBGColor: '#b4a913' }, 22 | piecesCount: { iconBGColor: '#8da136', valueBGColor: '#99ae3d' }, 23 | piecesLength: { iconBGColor: '#07659c', valueBGColor: '#0872af' }, 24 | status: { iconBGColor: '#938948', valueBGColor: '#9f9450' }, 25 | size: { iconBGColor: '#81008f', valueBGColor: '#9102a1' }, 26 | category: { iconBGColor: '#914820', valueBGColor: '#c9632c' }, 27 | }, 28 | } 29 | 30 | export default function useGetWidgetColors(widgetName) { 31 | const { isDarkMode } = useContext(DarkModeContext) 32 | const widgetColors = colors[isDarkMode ? DARK : LIGHT][widgetName] 33 | 34 | return widgetColors 35 | } 36 | -------------------------------------------------------------------------------- /web/src/components/Donate/DonateDialog.jsx: -------------------------------------------------------------------------------- 1 | // import ListItem from '@material-ui/core/ListItem' 2 | import DialogTitle from '@material-ui/core/DialogTitle' 3 | import DialogContent from '@material-ui/core/DialogContent' 4 | import DialogActions from '@material-ui/core/DialogActions' 5 | // import List from '@material-ui/core/List' 6 | import ButtonGroup from '@material-ui/core/ButtonGroup' 7 | import Button from '@material-ui/core/Button' 8 | import { useTranslation } from 'react-i18next' 9 | import { StyledDialog } from 'style/CustomMaterialUiStyles' 10 | import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' 11 | 12 | // const donateFrame = '' 13 | 14 | export default function DonateDialog({ onClose }) { 15 | const { t } = useTranslation() 16 | const ref = useOnStandaloneAppOutsideClick(onClose) 17 | 18 | return ( 19 | 20 | {t('Donate')} 21 | 22 | {/* */} 23 | {/* */} 24 | 25 | 26 | 27 | 28 | {/* */} 29 | {/* */} 30 | 31 | {/* */} 32 | {/* */} 33 | {/* eslint-disable-next-line react/no-danger */} 34 | {/*
*/} 35 | {/* */} 36 | {/* */} 37 | 38 | 39 | 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /web/src/components/Donate/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Button from '@material-ui/core/Button' 3 | import Snackbar from '@material-ui/core/Snackbar' 4 | import IconButton from '@material-ui/core/IconButton' 5 | import CreditCardIcon from '@material-ui/icons/CreditCard' 6 | import CloseIcon from '@material-ui/icons/Close' 7 | import { useTranslation } from 'react-i18next' 8 | import styled from 'styled-components' 9 | import { standaloneMedia } from 'style/standaloneMedia' 10 | 11 | import DonateDialog from './DonateDialog' 12 | 13 | const StyledSnackbar = styled(Snackbar)` 14 | ${standaloneMedia('margin-bottom: 90px')}; 15 | ` 16 | 17 | export default function DonateSnackbar() { 18 | const { t } = useTranslation() 19 | const [open, setOpen] = useState(false) 20 | const [snackbarOpen, setSnackbarOpen] = useState(true) 21 | 22 | const disableSnackbar = () => { 23 | setSnackbarOpen(false) 24 | localStorage.setItem('snackbarIsClosed', true) 25 | } 26 | 27 | return ( 28 | <> 29 | {open && setOpen(false)} />} 30 | 31 | 41 | 53 | 54 | 55 | 56 | 57 | 58 | } 59 | /> 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /web/src/components/FilterByCategory.jsx: -------------------------------------------------------------------------------- 1 | import ListItem from '@material-ui/core/ListItem' 2 | import ListItemIcon from '@material-ui/core/ListItemIcon' 3 | import ListItemText from '@material-ui/core/ListItemText' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | export default function FilterByCategory({ categoryKey, categoryName, setGlobalFilterCategory, icon }) { 7 | const onClick = () => { 8 | setGlobalFilterCategory(categoryKey) 9 | } 10 | const { t } = useTranslation() 11 | 12 | return ( 13 | <> 14 | 15 | {icon} 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /web/src/components/RemoveAll.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogActions, DialogTitle } from '@material-ui/core' 2 | import ListItem from '@material-ui/core/ListItem' 3 | import ListItemIcon from '@material-ui/core/ListItemIcon' 4 | import ListItemText from '@material-ui/core/ListItemText' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import { useState } from 'react' 7 | import { torrentsHost } from 'utils/Hosts' 8 | import { useTranslation } from 'react-i18next' 9 | 10 | import UnsafeButton from './UnsafeButton' 11 | 12 | const fnRemoveAll = () => { 13 | fetch(torrentsHost(), { 14 | method: 'post', 15 | body: JSON.stringify({ action: 'wipe' }), 16 | headers: { 17 | Accept: 'application/json, text/plain, */*', 18 | 'Content-Type': 'application/json', 19 | }, 20 | }) 21 | } 22 | 23 | export default function RemoveAll({ isOffline, isLoading }) { 24 | const { t } = useTranslation() 25 | const [open, setOpen] = useState(false) 26 | const closeDialog = () => setOpen(false) 27 | const openDialog = () => setOpen(true) 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {t('DeleteTorrents?')} 41 | 42 | 45 | 46 | } 49 | variant='contained' 50 | onClick={() => { 51 | fnRemoveAll() 52 | closeDialog() 53 | }} 54 | color='secondary' 55 | autoFocus 56 | > 57 | {t('OK')} 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /web/src/components/Settings/MobileAppSettings.jsx: -------------------------------------------------------------------------------- 1 | import { FormControlLabel, FormGroup, FormHelperText, Switch } from '@material-ui/core' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { SecondarySettingsContent, SettingSectionLabel } from './style' 5 | 6 | export default function MobileAppSettings({ isVlcUsed, setIsVlcUsed }) { 7 | const { t } = useTranslation() 8 | 9 | return ( 10 | 11 | {t('SettingsDialog.MobileAppSettings')} 12 | 13 | setIsVlcUsed(prev => !prev)} color='secondary' />} 15 | label={t('SettingsDialog.UseVLC')} 16 | labelPlacement='start' 17 | /> 18 | {t('SettingsDialog.UseVLCHint')} 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /web/src/components/Settings/SliderInput.jsx: -------------------------------------------------------------------------------- 1 | import { Grid, OutlinedInput, Slider } from '@material-ui/core' 2 | 3 | export default function SliderInput({ 4 | isProMode, 5 | title, 6 | value, 7 | setValue, 8 | sliderMin, 9 | sliderMax, 10 | inputMin, 11 | inputMax, 12 | step = 1, 13 | onBlurCallback, 14 | }) { 15 | const onBlur = ({ target: { value } }) => { 16 | if (value < inputMin) return setValue(inputMin) 17 | if (value > inputMax) return setValue(inputMax) 18 | 19 | onBlurCallback && onBlurCallback(value) 20 | } 21 | 22 | const onInputChange = ({ target: { value } }) => setValue(value === '' ? '' : Number(value)) 23 | const onSliderChange = (_, newValue) => setValue(newValue) 24 | 25 | return ( 26 | <> 27 |
{title}
28 | 29 | 30 | 31 | 39 | 40 | 41 | {isProMode && ( 42 | 43 | 51 | 52 | )} 53 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /web/src/components/Settings/defaultSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | CacheSize: 64, 3 | ReaderReadAHead: 95, 4 | PreloadCache: 50, 5 | UseDisk: false, 6 | TorrentsSavePath: '', 7 | RemoveCacheOnDrop: false, 8 | ForceEncrypt: false, 9 | RetrackersMode: 1, 10 | TorrentDisconnectTimeout: 30, 11 | EnableDebug: false, 12 | EnableDLNA: false, 13 | FriendlyName: '', 14 | EnableRutorSearch: false, 15 | EnableIPv6: false, 16 | DisableTCP: false, 17 | DisableUTP: false, 18 | DisableUPNP: false, 19 | DisableDHT: false, 20 | DisablePEX: false, 21 | DisableUpload: false, 22 | DownloadRateLimit: 0, 23 | UploadRateLimit: 0, 24 | ConnectionsLimit: 25, 25 | PeersListenPort: 0, 26 | ResponsiveMode: false, 27 | SslPort: 0, 28 | SslCert: '', 29 | SslKey: '', 30 | } 31 | -------------------------------------------------------------------------------- /web/src/components/Settings/index.jsx: -------------------------------------------------------------------------------- 1 | import ListItemIcon from '@material-ui/core/ListItemIcon' 2 | import ListItemText from '@material-ui/core/ListItemText' 3 | import { useState } from 'react' 4 | import SettingsIcon from '@material-ui/icons/Settings' 5 | import { useTranslation } from 'react-i18next' 6 | import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles' 7 | import { isStandaloneApp } from 'utils/Utils' 8 | 9 | import SettingsDialog from './SettingsDialog' 10 | 11 | export default function SettingsDialogButton({ isOffline, isLoading }) { 12 | const { t } = useTranslation() 13 | const [isDialogOpen, setIsDialogOpen] = useState(false) 14 | 15 | const handleClickOpen = () => setIsDialogOpen(true) 16 | const handleClose = () => setIsDialogOpen(false) 17 | 18 | return ( 19 |
20 | 21 | {isStandaloneApp ? ( 22 | <> 23 | 24 |
{t('SettingsDialog.Settings')}
25 | 26 | ) : ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | 34 | )} 35 |
36 | 37 | {isDialogOpen && } 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /web/src/components/Settings/tabComponents.jsx: -------------------------------------------------------------------------------- 1 | export const a11yProps = index => ({ 2 | id: `full-width-tab-${index}`, 3 | 'aria-controls': `full-width-tabpanel-${index}`, 4 | }) 5 | 6 | export const TabPanel = ({ children, value, index, ...other }) => ( 7 | 10 | ) 11 | -------------------------------------------------------------------------------- /web/src/components/TorrentList/AddFirstTorrent.jsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@material-ui/core' 2 | import { useState } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | import AddDialog from '../Add/AddDialog' 6 | import IconWrapper from './style' 7 | 8 | export default function AddFirstTorrent() { 9 | const { t } = useTranslation() 10 | const [isDialogOpen, setIsDialogOpen] = useState(false) 11 | const handleClickOpen = () => setIsDialogOpen(true) 12 | const handleClose = () => setIsDialogOpen(false) 13 | const primary = useTheme().palette.primary.main 14 | 15 | return ( 16 | <> 17 | handleClickOpen(true)} isButton> 18 | 26 |
{t('NoTorrentsAdded')}
27 |
28 | 29 | {isDialogOpen && } 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /web/src/components/TorrentList/NoServerConnection.jsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@material-ui/core' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import IconWrapper from './style' 5 | 6 | export default function NoServerConnection() { 7 | const { t } = useTranslation() 8 | const primary = useTheme().palette.primary.main 9 | 10 | return ( 11 | 12 | 19 |
{t('Offline')}
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /web/src/components/TorrentList/index.jsx: -------------------------------------------------------------------------------- 1 | import TorrentCard from 'components/TorrentCard' 2 | import CircularProgress from '@material-ui/core/CircularProgress' 3 | import { TorrentListWrapper, CenteredGrid } from 'components/App/style' 4 | // import { useTranslation } from 'react-i18next' 5 | 6 | import NoServerConnection from './NoServerConnection' 7 | import AddFirstTorrent from './AddFirstTorrent' 8 | 9 | export default function TorrentList({ isOffline, isLoading, sortABC, torrents, sortCategory }) { 10 | // const { t } = useTranslation() 11 | if (isLoading || isOffline || !torrents.length) { 12 | return ( 13 | 14 | {isOffline ? ( 15 | 16 | ) : isLoading ? ( 17 | 18 | ) : ( 19 | !torrents.length && 20 | )} 21 | 22 | ) 23 | } 24 | 25 | const filteredTorrents = torrents.filter(torrent => sortCategory === 'all' || torrent.category === sortCategory) 26 | 27 | return sortABC ? ( 28 | 29 | {filteredTorrents 30 | .sort((a, b) => a.title > b.title) 31 | .map(torrent => ( 32 | 33 | ))} 34 | 35 | ) : ( 36 | 37 | {filteredTorrents.map(torrent => ( 38 | 39 | ))} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /web/src/components/TorrentList/style.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | export default styled.div` 4 | ${({ 5 | isButton, 6 | theme: { 7 | addDialog: { notificationSuccessBGColor, languageSwitchBGColor }, 8 | }, 9 | }) => css` 10 | display: grid; 11 | place-items: center; 12 | padding: 20px 40px; 13 | border-radius: 5px; 14 | 15 | ${isButton && 16 | css` 17 | background: ${notificationSuccessBGColor}; 18 | transition: 0.2s; 19 | cursor: pointer; 20 | 21 | :hover { 22 | background: ${languageSwitchBGColor}; 23 | } 24 | `} 25 | 26 | lord-icon { 27 | width: 200px; 28 | height: 200px; 29 | } 30 | 31 | .icon-label { 32 | font-size: 20px; 33 | } 34 | `} 35 | ` 36 | -------------------------------------------------------------------------------- /web/src/components/UnsafeButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@material-ui/core' 2 | import { useEffect, useState } from 'react' 3 | 4 | export default function UnsafeButton({ timeout, children, disabled, ...props }) { 5 | const [timeLeft, setTimeLeft] = useState(timeout || 7) 6 | const [buttonDisabled, setButtonDisabled] = useState(disabled || timeLeft > 0) 7 | const handleTimerTick = () => { 8 | const newTimeLeft = timeLeft - 1 9 | setTimeLeft(newTimeLeft) 10 | if (newTimeLeft <= 0) { 11 | setButtonDisabled(disabled) 12 | } 13 | } 14 | const getTimerText = () => (!disabled && timeLeft > 0 ? ` (${timeLeft})` : '') 15 | useEffect(() => { 16 | if (disabled || !timeLeft) { 17 | return 18 | } 19 | const intervalId = setInterval(handleTimerTick, 1000) 20 | return () => clearInterval(intervalId) 21 | // eslint-disable-next-line 22 | }, [timeLeft]) 23 | 24 | return ( 25 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /web/src/components/categories.jsx: -------------------------------------------------------------------------------- 1 | import MovieCreationIcon from '@material-ui/icons/MovieCreation' 2 | import LiveTvIcon from '@material-ui/icons/LiveTv' 3 | import MusicNoteIcon from '@material-ui/icons/MusicNote' 4 | import MoreHorizIcon from '@material-ui/icons/MoreHoriz' 5 | 6 | export const TORRENT_CATEGORIES = [ 7 | { key: 'movie', name: 'Movies', icon: }, 8 | { key: 'tv', name: 'Series', icon: }, 9 | { key: 'music', name: 'Music', icon: }, 10 | { key: 'other', name: 'Other', icon: }, 11 | ] 12 | -------------------------------------------------------------------------------- /web/src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | import translationEN from 'locales/en/translation.json' 5 | import translationRU from 'locales/ru/translation.json' 6 | import translationUA from 'locales/ua/translation.json' 7 | import translationZH from 'locales/zh/translation.json' 8 | import translationBG from 'locales/bg/translation.json' 9 | 10 | i18n 11 | .use(LanguageDetector) 12 | .use(initReactI18next) 13 | .init({ 14 | fallbackLng: 'en', // default language will be used if none of declared lanuages detected (en, ru) 15 | interpolation: { escapeValue: false }, // react already safes from xss 16 | resources: { 17 | en: { translation: translationEN }, 18 | ru: { translation: translationRU }, 19 | ua: { translation: translationUA }, 20 | zh: { translation: translationZH }, 21 | bg: { translation: translationBG }, 22 | }, 23 | }) 24 | 25 | export default i18n 26 | -------------------------------------------------------------------------------- /web/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { QueryClientProvider, QueryClient } from 'react-query' 4 | 5 | import App from './components/App' 6 | import 'i18n' 7 | 8 | const queryClient = new QueryClient() 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ) 18 | -------------------------------------------------------------------------------- /web/src/style/CustomMaterialUiStyles.js: -------------------------------------------------------------------------------- 1 | import { ListItem } from '@material-ui/core' 2 | import Dialog from '@material-ui/core/Dialog' 3 | import { pwaFooterHeight } from 'components/App/PWAFooter/style' 4 | import styled, { css } from 'styled-components' 5 | import { Header } from 'style/DialogStyles' 6 | import { isStandaloneApp } from 'utils/Utils' 7 | 8 | import { standaloneMedia } from './standaloneMedia' 9 | 10 | export const StyledMenuButtonWrapper = styled(ListItem).attrs({ button: true })` 11 | ${standaloneMedia(css` 12 | width: 100%; 13 | height: 60px; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | font-size: 10px; 19 | `)} 20 | ` 21 | 22 | export const StyledDialog = styled(Dialog).attrs({ 23 | ...(isStandaloneApp && { hideBackdrop: true, transitionDuration: 0 }), 24 | })` 25 | ${standaloneMedia(css` 26 | margin-bottom: ${pwaFooterHeight}px; 27 | 28 | .MuiDialog-container .MuiPaper-root { 29 | box-shadow: none; 30 | } 31 | `)} 32 | ` 33 | 34 | export const StyledHeader = styled(Header)` 35 | ${standaloneMedia(css` 36 | padding-top: 47px; 37 | `)} 38 | ` 39 | -------------------------------------------------------------------------------- /web/src/style/DialogStyles.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | export const Header = styled.div` 4 | ${({ theme: { primary } }) => css` 5 | background: ${primary}; 6 | color: rgba(0, 0, 0, 0.87); 7 | font-size: 20px; 8 | color: #fff; 9 | font-weight: 600; 10 | box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); 11 | padding: 15px 24px; 12 | position: relative; 13 | `} 14 | ` 15 | 16 | export const ButtonWrapper = styled.div` 17 | padding: 20px; 18 | display: flex; 19 | justify-content: flex-end; 20 | 21 | > :not(:last-child) { 22 | margin-right: 10px; 23 | } 24 | ` 25 | -------------------------------------------------------------------------------- /web/src/style/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle, css } from 'styled-components' 2 | 3 | import { standaloneMedia } from './standaloneMedia' 4 | 5 | export default createGlobalStyle` 6 | *, 7 | *::before, 8 | *::after { 9 | margin: 0; 10 | padding: 0; 11 | box-sizing: inherit; 12 | } 13 | 14 | body { 15 | font-family: "Open Sans", sans-serif; 16 | box-sizing: border-box; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | letter-spacing: -0.1px; 20 | -webkit-tap-highlight-color: transparent; 21 | 22 | 23 | ${standaloneMedia(css` 24 | height: 100vh; 25 | `)} 26 | } 27 | 28 | button { 29 | font-family: "Open Sans", sans-serif; 30 | letter-spacing: -0.1px; 31 | } 32 | ` 33 | -------------------------------------------------------------------------------- /web/src/style/getStyledComponentsTheme.js: -------------------------------------------------------------------------------- 1 | import { mainColors, themeColors } from './colors' 2 | 3 | export default type => ({ ...themeColors[type], ...mainColors[type] }) 4 | -------------------------------------------------------------------------------- /web/src/style/materialUISetup.js: -------------------------------------------------------------------------------- 1 | import { createTheme, useMediaQuery } from '@material-ui/core' 2 | import { useEffect, useMemo, useState } from 'react' 3 | 4 | import { mainColors, themeColors } from './colors' 5 | 6 | export const THEME_MODES = { LIGHT: 'light', DARK: 'dark', AUTO: 'auto' } 7 | 8 | const typography = { fontFamily: 'Open Sans, sans-serif' } 9 | 10 | export const darkTheme = createTheme({ 11 | typography, 12 | palette: { 13 | type: THEME_MODES.DARK, 14 | primary: { main: mainColors.dark.primary }, 15 | secondary: { main: mainColors.dark.secondary }, 16 | }, 17 | }) 18 | export const lightTheme = createTheme({ 19 | typography, 20 | palette: { 21 | type: THEME_MODES.LIGHT, 22 | primary: { main: mainColors.light.primary }, 23 | secondary: { main: mainColors.light.secondary }, 24 | }, 25 | }) 26 | 27 | export const useMaterialUITheme = () => { 28 | const savedThemeMode = localStorage.getItem('themeMode') 29 | const isSystemModeDark = useMediaQuery('(prefers-color-scheme: dark)') 30 | const [isDarkMode, setIsDarkMode] = useState(savedThemeMode === 'dark' || isSystemModeDark) 31 | const [currentThemeMode, setCurrentThemeMode] = useState(savedThemeMode || THEME_MODES.AUTO) 32 | 33 | const updateThemeMode = mode => { 34 | setCurrentThemeMode(mode) 35 | localStorage.setItem('themeMode', mode) 36 | } 37 | 38 | useEffect(() => { 39 | currentThemeMode === THEME_MODES.LIGHT && setIsDarkMode(false) 40 | currentThemeMode === THEME_MODES.DARK && setIsDarkMode(true) 41 | currentThemeMode === THEME_MODES.AUTO && setIsDarkMode(isSystemModeDark) 42 | }, [isSystemModeDark, currentThemeMode]) 43 | 44 | const theme = isDarkMode ? THEME_MODES.DARK : THEME_MODES.LIGHT 45 | 46 | const muiTheme = useMemo( 47 | () => 48 | createTheme({ 49 | typography, 50 | palette: { 51 | type: theme, 52 | primary: { main: mainColors[theme].primary }, 53 | secondary: { main: mainColors[theme].secondary }, 54 | }, 55 | overrides: { 56 | MuiTypography: { 57 | h6: { 58 | fontSize: '1.0rem', 59 | }, 60 | }, 61 | MuiPaper: { 62 | root: { 63 | backgroundColor: themeColors[theme].app.paperColor, 64 | }, 65 | }, 66 | MuiInputBase: { 67 | input: { 68 | color: mainColors[theme].labels, 69 | }, 70 | }, 71 | // https://material-ui.com/ru/api/form-control-label/ 72 | MuiFormControlLabel: { 73 | labelPlacementStart: { 74 | display: 'flex', 75 | justifyContent: 'space-between', 76 | marginStart: 0, 77 | marginTop: 6, 78 | marginBottom: 2, 79 | }, 80 | }, 81 | MuiInputLabel: { 82 | root: { 83 | color: mainColors[theme].labels, 84 | marginBottom: 8, 85 | '&$focused': { 86 | color: mainColors[theme].labels, 87 | }, 88 | }, 89 | }, 90 | MuiFormGroup: { 91 | root: { 92 | '& .MuiFormHelperText-root': { 93 | marginTop: -8, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }), 99 | [theme], 100 | ) 101 | 102 | return [isDarkMode, currentThemeMode, updateThemeMode, muiTheme] 103 | } 104 | -------------------------------------------------------------------------------- /web/src/style/standaloneMedia.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components' 2 | 3 | export const standaloneMedia = styles => css` 4 | @media screen and (display-mode: standalone) { 5 | ${styles}; 6 | } 7 | ` 8 | -------------------------------------------------------------------------------- /web/src/torrentStates.js: -------------------------------------------------------------------------------- 1 | export const [GETTING_INFO, PRELOAD, WORKING, CLOSED, IN_DB] = [1, 2, 3, 4, 5] 2 | -------------------------------------------------------------------------------- /web/src/utils/Hosts.js: -------------------------------------------------------------------------------- 1 | const { protocol, hostname, port } = window.location 2 | 3 | let torrserverHost = process.env.REACT_APP_SERVER_HOST || `${protocol}//${hostname}${port ? `:${port}` : ''}` 4 | 5 | export const torrentsHost = () => `${torrserverHost}/torrents` 6 | export const viewedHost = () => `${torrserverHost}/viewed` 7 | export const cacheHost = () => `${torrserverHost}/cache` 8 | export const torrentUploadHost = () => `${torrserverHost}/torrent/upload` 9 | export const settingsHost = () => `${torrserverHost}/settings` 10 | export const streamHost = () => `${torrserverHost}/stream` 11 | export const shutdownHost = () => `${torrserverHost}/shutdown` 12 | export const echoHost = () => `${torrserverHost}/echo` 13 | export const playlistTorrHost = () => `${torrserverHost}/stream` 14 | 15 | export const getTorrServerHost = () => torrserverHost 16 | export const setTorrServerHost = host => { 17 | torrserverHost = host 18 | } 19 | -------------------------------------------------------------------------------- /web/src/utils/Utils.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import i18n from '../i18n' 4 | import { torrentsHost } from './Hosts' 5 | 6 | export function humanizeSize(size) { 7 | if (!size) return '' 8 | const i = Math.floor(Math.log(size) / Math.log(1024)) 9 | return `${(size / Math.pow(1024, i)).toFixed(2) * 1} ${ 10 | [i18n.t('B'), i18n.t('KB'), i18n.t('MB'), i18n.t('GB'), i18n.t('TB')][i] 11 | }` 12 | } 13 | 14 | export function humanizeSpeed(speed) { 15 | if (!speed) return '' 16 | const i = Math.floor(Math.log(speed * 8) / Math.log(1000)) 17 | return `${((speed * 8) / Math.pow(1000, i)).toFixed(0) * 1} ${ 18 | [i18n.t('bps'), i18n.t('kbps'), i18n.t('Mbps'), i18n.t('Gbps'), i18n.t('Tbps')][i] 19 | }` 20 | } 21 | 22 | export function getPeerString(torrent) { 23 | if (!torrent || !torrent.active_peers) return null 24 | const seeders = typeof torrent.connected_seeders !== 'undefined' ? torrent.connected_seeders : 0 25 | return `${torrent.active_peers} / ${torrent.total_peers} · ${seeders}` 26 | } 27 | 28 | export const shortenText = (text, sympolAmount) => 29 | text ? text.slice(0, sympolAmount) + (text.length > sympolAmount ? '…' : '') : '' 30 | 31 | export const removeRedundantCharacters = string => { 32 | let newString = string 33 | const brackets = [ 34 | ['(', ')'], 35 | ['[', ']'], 36 | ['{', '}'], 37 | ] 38 | 39 | brackets.forEach(el => { 40 | const leftBracketRegexFormula = `\\${el[0]}` 41 | const leftBracketRegex = new RegExp(leftBracketRegexFormula, 'g') 42 | const leftBracketAmount = [...newString.matchAll(leftBracketRegex)].length 43 | const rightBracketRegexFormula = `\\${el[1]}` 44 | const rightBracketRegex = new RegExp(rightBracketRegexFormula, 'g') 45 | const rightBracketAmount = [...newString.matchAll(rightBracketRegex)].length 46 | 47 | if (leftBracketAmount !== rightBracketAmount) { 48 | const removeFormula = `(\\${el[0]})(?!.*\\1).*` 49 | const removeRegex = new RegExp(removeFormula, 'g') 50 | newString = newString.replace(removeRegex, '') 51 | } 52 | }) 53 | 54 | const hasThreeDotsAtTheEnd = !!newString.match(/\.{3}$/g) 55 | 56 | const trimmedString = newString.replace(/[\\.| ]+$/g, '').trim() 57 | 58 | return hasThreeDotsAtTheEnd ? `${trimmedString}..` : trimmedString 59 | } 60 | 61 | export const getTorrents = async () => { 62 | try { 63 | const { data } = await axios.post(torrentsHost(), { action: 'list' }) 64 | return data 65 | } catch (error) { 66 | throw new Error(null) 67 | } 68 | } 69 | 70 | export const isStandaloneApp = window.matchMedia('screen and (display-mode: standalone)').matches 71 | -------------------------------------------------------------------------------- /web/src/utils/checkIsIOS.jsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | if (typeof window === `undefined` || typeof navigator === `undefined`) return false 3 | 4 | return /iPhone|iPad|iPod/i.test(navigator.userAgent || navigator.vendor) 5 | } 6 | -------------------------------------------------------------------------------- /web/src/utils/useChangeLanguage.js: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | 3 | export default () => { 4 | const { i18n } = useTranslation() 5 | const currentLanguage = i18n.language.substr(0, 2) 6 | 7 | return [currentLanguage, lang => i18n.changeLanguage(lang)] 8 | } 9 | -------------------------------------------------------------------------------- /web/src/utils/useOnStandaloneAppOutsideClick.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { isStandaloneApp } from 'utils/Utils' 3 | 4 | export default function useOnStandaloneAppOutsideClick(onClickOutside) { 5 | const ref = useRef() 6 | 7 | useEffect(() => { 8 | if (!isStandaloneApp) return 9 | 10 | const handleClickOutside = event => { 11 | if (ref.current && !ref.current.contains(event.target)) { 12 | onClickOutside && onClickOutside() 13 | } 14 | } 15 | 16 | document.addEventListener('click', handleClickOutside, true) 17 | 18 | return () => { 19 | document.removeEventListener('click', handleClickOutside, true) 20 | } 21 | }) 22 | 23 | return ref 24 | } 25 | -------------------------------------------------------------------------------- /web/src/utils/usePreviousState.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export default function usePreviousState(value) { 4 | const ref = useRef(value) 5 | 6 | useEffect(() => { 7 | ref.current = value 8 | }, [value]) 9 | 10 | return ref.current 11 | } 12 | --------------------------------------------------------------------------------