├── .dockerignore ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── docker.yaml ├── .gitignore ├── API.md ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── 1.png ├── 2.png ├── 3.png ├── CNY.png ├── ci.gz ├── emule.jpeg ├── index.png ├── like.png └── new_resource.png ├── conf ├── yyets.dmesg.app.conf └── yyets.env ├── docker-compose.yml ├── requirements.txt ├── scripts ├── install.sh └── migrate_sub.py ├── setup.py ├── tea.yaml ├── yyets ├── BagAndDrag │ ├── README.md │ ├── bag.py │ ├── cfkv.py │ ├── convert_db.py │ ├── create_db.py │ ├── drag.py │ ├── sample.json │ └── zimuxia │ │ ├── convert_db.py │ │ └── zimuxia.py ├── __init__.py ├── healthcheck │ ├── check.py │ └── restart_service.py ├── management │ ├── format.json │ └── ui.py └── worker │ ├── .cargo-ok │ ├── README.md │ ├── public │ ├── 404.html │ ├── css │ │ ├── 3rd │ │ │ ├── animate.css │ │ │ ├── icons.css │ │ │ └── widgets.css │ │ ├── aYin.css │ │ ├── bootstrap.min.css │ │ ├── data.json │ │ ├── down-list-20180530.css │ │ ├── font-awesome.min.css │ │ ├── index.json │ │ └── jquery.mCustomScrollbar.css │ ├── favicon.ico │ ├── fonts │ │ ├── fontawesome-webfont.woff2 │ │ └── test.txt │ ├── img │ │ ├── 11bcd4d0f2daf8b02fecc72bc8ca38ab.png │ │ ├── 200-wrangler-ferris.gif │ │ ├── 404-wrangler-ferris.gif │ │ ├── grid16.png │ │ └── yyetsTrans.png │ ├── index.html │ ├── js │ │ ├── aYin.js │ │ ├── bootstrap.min.js │ │ ├── jquery.mCustomScrollbar.min.js │ │ ├── jquery.min.js │ │ ├── jquery.mousewheel.min.js │ │ ├── rshare.js │ │ ├── search.js │ │ └── vue.js │ ├── resource.html │ └── search.html │ ├── workers-site │ ├── index.js │ ├── package-lock.json │ └── package.json │ └── wrangler.toml ├── yyetsbot ├── config.py ├── fansub.py ├── utils.py ├── warning.webp └── yyetsbot.py └── yyetsweb ├── README.md ├── YYeTs-grafana.json ├── commands ├── common.py ├── douban_fix.py ├── grafana_test_data.py └── share_excel.py ├── common ├── __init__.py ├── dump_db.py ├── sync.py └── utils.py ├── databases ├── __init__.py ├── base.py ├── comment.py ├── douban.py ├── grafana.py ├── oauth.py ├── other.py ├── resources.py └── user.py ├── go.mod ├── go.sum ├── handlers ├── __init__.py ├── base.py ├── comment.py ├── douban.py ├── grafana.py ├── oauth.py ├── other.py ├── resources.py └── user.py ├── server.go ├── server.py ├── templates ├── 404.html ├── ads.txt ├── css │ ├── 3rd │ │ ├── animate.css │ │ ├── icons.css │ │ └── widgets.css │ ├── aYin.css │ ├── bootstrap.min.css │ ├── data.json │ ├── down-list-20180530.css │ ├── font-awesome.min.css │ ├── index.json │ ├── jquery.mCustomScrollbar.css │ ├── normalize.min.css │ └── noty.css ├── email_template.html ├── favicon.ico ├── fonts │ └── fontawesome-webfont.woff2 ├── googlee927c64a054a7beb.html ├── help.html ├── img │ ├── 11bcd4d0f2daf8b02fecc72bc8ca38ab.png │ ├── 200-wrangler-ferris.gif │ ├── 404-wrangler-ferris.gif │ ├── afdian.png │ ├── default-green.png │ ├── grid16.png │ └── yyetsTrans.png ├── index.html ├── js │ ├── aYin.js │ ├── axios.min.js │ ├── bootstrap.min.js │ ├── common.js │ ├── jquery.mCustomScrollbar.min.js │ ├── jquery.min.js │ ├── jquery.mousewheel.min.js │ ├── noty.min.js │ ├── rshare.js │ ├── sample.json │ └── vue.js ├── resource.html ├── robots.txt └── search.html └── tests └── router_test.py /.dockerignore: -------------------------------------------------------------------------------- 1 | mongo_data/* 2 | certs/* 3 | data/* 4 | logs/* 5 | YYeTsFE/node_modules/* 6 | .github/* 7 | assets/* 8 | conf/* 9 | tests/* 10 | yyetsweb/yyets.sqlite 11 | yyetsweb/subtitle_data -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yyets/worker/public/css/** linguist-vendored 2 | yyets/worker/public/fonts/* linguist-vendored 3 | yyets/worker/public/img/* linguist-vendored 4 | yyets/worker/public/js/* linguist-vendored 5 | yyets/worker/public/js/search.js -linguist-vendored 6 | yyets/worker/public/404.html linguist-vendored 7 | yyets/worker/public/resource.html linguist-vendored 8 | 9 | yyetsweb/templates/css/** linguist-vendored 10 | yyetsweb/templates/fonts/* linguist-vendored 11 | yyetsweb/templates/img/* linguist-vendored 12 | yyetsweb/templates/js/* linguist-vendored 13 | yyetsweb/templates/404.html linguist-vendored 14 | yyetsweb/templates/resource.html linguist-vendored 15 | 16 | yyetsweb/templates/js/common.js -linguist-vendored 17 | 18 | tests/data/* linguist-vendored 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: BennyThink 4 | custom: https://buy.stripe.com/dR67vU4p13Ox73a6oq 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | all-dependencies: 9 | patterns: ["*"] -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push docker image 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | paths-ignore: 7 | - '**.md' 8 | - 'LICENSE' 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | submodules: true 18 | 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | 25 | - name: Cache Docker layers 26 | uses: actions/cache@v3 27 | with: 28 | path: /tmp/.buildx-cache 29 | key: ${{ runner.os }}-buildx-${{ github.sha }} 30 | restore-keys: | 31 | ${{ runner.os }}-buildx- 32 | 33 | - name: Login to DockerHub 34 | uses: docker/login-action@v2 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@v2 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.repository_owner }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Lower case for Docker Hub 47 | id: dh_string 48 | uses: ASzc/change-string-case-action@v5 49 | with: 50 | string: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} 51 | 52 | - name: Lower case for ghcr 53 | id: ghcr_string 54 | uses: ASzc/change-string-case-action@v5 55 | with: 56 | string: ${{ github.event.repository.full_name }} 57 | 58 | - name: Build and push docker images 59 | uses: docker/build-push-action@v4 60 | with: 61 | context: . 62 | platforms: linux/amd64, linux/arm64 63 | push: true 64 | tags: | 65 | ${{ steps.dh_string.outputs.lowercase }} 66 | ghcr.io/${{ steps.ghcr_string.outputs.lowercase }} 67 | cache-from: type=local,src=/tmp/.buildx-cache 68 | cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max 69 | 70 | - name: Move cache 71 | run: | 72 | rm -rf /tmp/.buildx-cache 73 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 74 | 75 | - name: Notification to Telegram 76 | env: 77 | TOKEN: ${{ secrets.BOT_TOKEN }} 78 | run: | 79 | curl "https://api.telegram.org/bot$TOKEN/sendMessage?chat_id=260260121&text=Normal%20Build%20complete!" 80 | echo "YYeTsBot Build complete!" 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | #*.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | *.dat 54 | *.dir 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | /.idea/$CACHE_FILE$ 108 | /.idea/.gitignore 109 | /.idea/misc.xml 110 | /.idea/modules.xml 111 | /.idea/vcs.xml 112 | .idea/ 113 | /yyetsbot/data/cookies.dump 114 | /.idea/inspectionProfiles/profiles_settings.xml 115 | yyetsbot/data/ 116 | health_check/client.session 117 | 118 | /tools/worker/.idea/ 119 | /tools/worker/workers-site/node_modules/* 120 | /tools/worker/workers-site/worker/script.js 121 | /health_check/client-hc.session 122 | mongo_data/* 123 | certs/* 124 | data/* 125 | logs/* 126 | **/.DS_Store 127 | /yyetsweb/yyets_sqlite.db 128 | /yyetsweb/yyetsweb 129 | /yyetsweb/assets.go 130 | /yyetsweb/templates/sponsor/* 131 | /yyetsweb/templates/svg/* 132 | /yyetsweb/templates/index.css 133 | /yyetsweb/templates/logo* 134 | /yyetsweb/templates/*.json 135 | 136 | /yyetsweb/templates/* 137 | /yyetsweb/templates/data/* 138 | /yyetsweb/builds/ 139 | /builds/checksum-sha256sum.txt 140 | /yyetsweb/1.html 141 | !/env/ 142 | !/mongo_data/ 143 | /env/yyets.env 144 | !/docker-compose.override.yml 145 | /docker-compose.override.yml 146 | /yyetsweb/templates/dump/yyets_mongo.gz 147 | /yyetsweb/templates/dump/yyets_mysql.zip 148 | /yyetsweb/templates/dump/yyets_sqlite.zip 149 | /yyetsweb/subtitle_data/attachment/201001/17/758231_1263706947i2nW.rar 150 | /yyetsweb/subtitle_data/attachment/200912/4/212807_1259889699DJm8.rar 151 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # 项目手册 2 | 3 | # 网站部署方式 4 | 5 | ## 一键脚本 6 | 7 | **支持amd64/arm64,请先安装 docker、docker-compose和curl** 8 | 9 | **为了安全考虑,安装完成后程序将监听在 127.0.0.1 。如有需要请自行修改 `docker-compose.yml`的127.0.0.1为0.0.0.0** 10 | 11 | ### Linux/macOS: 12 | 13 | ```bash 14 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/master/scripts/install.sh)" 15 | ```` 16 | 17 | ### Windows 18 | 19 | 请再安装一个 [git for windows](https://gitforwindows.org/),然后桌面空白处右键,选择 `git bash here` 20 | 再然后 21 | 22 | ```bash 23 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/master/scripts/install.sh)" 24 | ```` 25 | 26 | ## docker-compose 27 | 28 | 参考 `yyetsweb`目录下的 `README` 29 | 30 | # bot 31 | 32 | 可以选择docker,也可以直接运行在机器上。 33 | 34 | ## docker-compose 35 | 36 | * 参见 [这里](https://github.com/tgbot-collection/BotsRunner) 37 | * 本目录下的 `docker-compose.yml` 也可以作为参考 38 | * nginx reverse proxy可以[参考这里](https://github.com/BennyThink/WebsiteRunner) 39 | * [参考这里获取数据库](yyetsweb/README.md) 40 | 41 | ```shell 42 | # 启动数据库 43 | docker-compose up -d mongo 44 | # 导入数据库 45 | docker yyets_mongo.gz 1234da:/tmp 46 | # 进入容器 47 | docker-compose exec mongo bash 48 | mongorestore --gzip --archive=yyets_mongo.gz --nsFrom "share.*" --nsTo "zimuzu.*" 49 | exit 50 | # 开启服务 51 | docker-compose up -d 52 | ``` 53 | 54 | ## replica set 配置方式 55 | 56 | ```shell 57 | ln -s docker-compose-replica.yml docker-compose.override.yml 58 | docker-compose up -d mongo 59 | # 进入shell 60 | rs.initiate({ 61 | _id: "rs0", 62 | members: [{ 63 | _id: 0, 64 | host: "localhost:27017" 65 | }, 66 | { 67 | _id: 1, 68 | host: "mongo2:27017" 69 | }] 70 | }) 71 | 72 | # 调整优先级 73 | cfg = rs.conf() 74 | cfg.members[0].priority = 0.5 75 | cfg.members[1].priority = 0.5 76 | cfg.members[2].priority = 1 # 最高 77 | rs.reconfig(cfg) 78 | 79 | ``` 80 | 81 | ## 常规方式 82 | 83 | ### 1. 环境 84 | 85 | 推荐使用Python 3.6+,环境要求 86 | 87 | * redis 88 | * 可选MongoDB 89 | 90 | ```bash 91 | pip install -r requirements.txt 92 | ``` 93 | 94 | ### 2. 配置TOKEN 95 | 96 | 修改`config.py`,根据需求修改如下配置项 97 | 98 | * TOKEN:bot token 99 | * USERNAME:人人影视的有效的用户名 100 | * PASSWORD :人人影视的有效的密码 101 | * MAINTAINER:维护者的Telegram UserID 102 | * REDIS:redis的地址,一般为localhost 103 | * MONGODB: mongodb的地址 104 | 105 | ### 3. 导入数据(可选) 106 | 107 | 如果使用yyets,那么需要导入数据到MongoDB。可以在将数据导入到MySQL之后使用如下脚本导入数据到MongoDB 108 | 109 | ```shell 110 | python3 web/prepare/convert_db.py 111 | ``` 112 | 113 | ### 4. 运行 114 | 115 | ```bash 116 | python /path/to/YYeTsBot/yyetsbot/bot.py 117 | ``` 118 | 119 | ### 5. systemd 单元文件 120 | 121 | 参考 `yyets.service` 122 | 123 | # 添加新的资源网站 124 | 125 | 欢迎各位开发提交新的资源网站!方法非常简单,重写 `BaseFansub`,实现`search_preview`和`search_result`,按照约定的格式返回数据。 126 | 127 | 然后把类名字添加到 `FANSUB_ORDER` 就可以了!是不是很简单! 128 | 129 | # 防爬 130 | 131 | ## 1. referer 132 | 133 | 网站使用referer验证请求 134 | 135 | ## 2. rate limit 136 | 137 | 404的访问会被计数,超过10次会被拉入黑名单,持续3600秒,再次访问会持续叠加。 138 | 139 | # 持续部署 140 | 141 | 使用[Docker Hub Webhook](https://docs.docker.com/docker-hub/webhooks/) 142 | (顺便吐槽一句,这是个什么垃圾文档……自己实现validation吧) 143 | 144 | 参考listener [Webhook listener](https://github.com/tgbot-collection/Webhook) 145 | 146 | # 归档资源下载 147 | 148 | ## Telegram 频道分享 149 | 150 | * 151 | 152 | 包含了2021年1月11日为止的人人影视最新资源,MySQL为主。有兴趣的盆友可以用这个数据进行二次开发[戳我查看详情](https://t.me/mikuri520/668) 153 | 154 | * 字幕侠离线数据库 [从这里下载](https://t.me/mikuri520/715),这个数据比较粗糙,并且字幕侠网站还在,因此不建议使用这个 155 | 156 | ## 本地下载 157 | 158 | 如果无法访问Telegram,可以使用如下网址下载数据 159 | 160 | * [MongoDB](https://yyets.click/data/yyets_mongo.gz) 161 | * [MySQL](https://yyets.click/data/yyets_mysql.zip) 162 | * [SQLite](https://yyets.click/data/yyets_sqlite.zip) 163 | 164 | # API 文档 165 | 166 | 参考 [API.md](API.md) 167 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine AS pybuilder 2 | RUN apk update && apk add --no-cache tzdata ca-certificates alpine-sdk libressl-dev libffi-dev cargo && \ 3 | apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ 4 | libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev libxcb-dev libpng-dev 5 | 6 | COPY requirements.txt /requirements.txt 7 | RUN pip3 install --user -r /requirements.txt && rm /requirements.txt 8 | 9 | 10 | FROM python:3.12-alpine AS runner 11 | RUN apk update && apk add --no-cache libressl jpeg-dev openjpeg-dev libimagequant-dev tiff-dev freetype-dev libxcb-dev 12 | 13 | 14 | FROM alpine AS nodebuilder 15 | RUN apk add curl jq 16 | RUN wget $(curl -s https://api.github.com/repos/tgbot-collection/YYeTsFE/releases/tags/ads-2025-03-27 | jq -r '.assets[] | select(.name == "build.zip") | .browser_download_url') 17 | RUN unzip build.zip && rm build.zip 18 | 19 | FROM runner 20 | RUN apk add mongodb-tools mysql-client 21 | COPY . /YYeTsBot 22 | 23 | COPY --from=pybuilder /root/.local /usr/local 24 | COPY --from=pybuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 25 | COPY --from=pybuilder /usr/share/zoneinfo /usr/share/zoneinfo 26 | COPY --from=nodebuilder /build /YYeTsBot/yyetsweb/templates/ 27 | 28 | ENV TZ=Asia/Shanghai 29 | WORKDIR /YYeTsBot/yyetsbot 30 | CMD ["python", "yyetsbot.py"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Benny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OS = darwin linux windows 2 | ARCH = amd64 arm64 3 | WEB := $(shell cd yyetsweb;pwd) 4 | DATE:=$(shell date +"%Y-%m-%d %H:%M:%S") 5 | update: 6 | git pull 7 | git submodule update --remote 8 | 9 | docker-dev: 10 | make update 11 | docker build --build-arg env=dev -t bennythink/yyetsbot . 12 | docker-compose up -d 13 | 14 | docker: 15 | # production configuration 16 | cp .env YYeTsFE/.env 17 | # docker buildx create --use --name mybuilder 18 | docker buildx build --platform=linux/amd64,linux/arm64 -t bennythink/yyetsbot . --push 19 | 20 | 21 | clean: 22 | docker rmi bennythink/yyetsbot:latest || true 23 | rm -rf YYeTsFE/build 24 | rm -rf YYeTsFE/dist 25 | 26 | @rm -rf yyetsweb/builds 27 | @rm -f yyetsweb/assets.go 28 | 29 | current: 30 | echo "Installing dependencies..." 31 | cd $(WEB);go get -u github.com/go-bindata/go-bindata/... 32 | echo "Build static files..." 33 | make asset 34 | echo "Build current platform executable..." 35 | cd $(WEB); go build .; 36 | 37 | asset: 38 | cd $(WEB);go get -u github.com/go-bindata/go-bindata/... ;go install github.com/go-bindata/go-bindata/... 39 | cd $(WEB)/templates;~/go/bin/go-bindata -o assets.go ./... 40 | mv yyetsweb/templates/assets.go yyetsweb/assets.go 41 | 42 | frontend: 43 | cd YYeTsFE; yarn && yarn run release 44 | cp -R YYeTsFE/build/* yyetsweb/templates/ 45 | 46 | all: 47 | make clean 48 | make frontend 49 | make asset 50 | @echo "Build all platform executables..." 51 | @for o in $(OS) ; do \ 52 | for a in $(ARCH) ; do \ 53 | echo "Building $$o-$$a..."; \ 54 | if [ "$$o" = "windows" ]; then \ 55 | cd $(WEB);CGO_ENABLED=0 GOOS=$$o GOARCH=$$a go build -ldflags="-s -w -X 'main.buildTime=$(DATE)'" -o builds/yyetsweb-$$o-$$a.exe .; \ 56 | else \ 57 | cd $(WEB);CGO_ENABLED=0 GOOS=$$o GOARCH=$$a go build -ldflags="-s -w -X 'main.buildTime=$(DATE)'" -o builds/yyetsweb-$$o-$$a .; \ 58 | fi; \ 59 | done \ 60 | done 61 | 62 | @make universal 63 | @make checksum 64 | 65 | 66 | checksum: yyetsweb/builds/* 67 | @echo "Generating checksums..." 68 | if [ "$(shell uname)" = "Darwin" ]; then \ 69 | shasum -a 256 $^ >> $(WEB)/builds/checksum-sha256sum.txt ;\ 70 | else \ 71 | sha256sum $^ >> $(WEB)/builds/checksum-sha256sum.txt; \ 72 | fi 73 | 74 | 75 | universal: 76 | @echo "Building macOS universal binary..." 77 | docker run --rm -v $(WEB)/builds:/app/ bennythink/lipo-linux -create -output \ 78 | yyetsweb-darwin-universal \ 79 | yyetsweb-darwin-amd64 yyetsweb-darwin-arm64 80 | 81 | file $(WEB)/builds/yyetsweb-darwin-universal 82 | 83 | release: 84 | git tag $(shell git rev-parse --short HEAD) 85 | git push --tags 86 | 87 | 88 | ci-test: 89 | docker run --rm bennythink/yyetsbot /bin/sh -c "cd /YYeTsBot/yyetsweb/tests;python -m unittest discover -p '*_test.py'" 90 | 91 | test: 92 | cd $(WEB)/tests;python -m unittest discover -p '*_test.py' 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YYeTsBot 2 | 3 | [![build docker image](https://github.com/tgbot-collection/YYeTsBot/actions/workflows/docker.yaml/badge.svg)](https://github.com/tgbot-collection/YYeTsBot/actions/workflows/docker.yaml) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/bennythink/yyetsbot)](https://hub.docker.com/r/bennythink/yyetsbot) 5 | 6 | ![](assets/index.png) 7 | 8 | 👉 前端[在这里](https://github.com/tgbot-collection/YYeTsFE) 👈 9 | 10 | # 使用说明 11 | 12 | 直接发送想要看的剧集名称就可以了,可选分享网页或者链接(ed2k和磁力链接)。 13 | 14 | 15 | 搜索资源时,会按照我预定的优先级(人人影视离线、字幕侠)进行搜索,当然也可以使用命令强制某个字幕组,如 `/yyets_offline 逃避可耻` 16 | 17 | ## 命令 18 | 19 | ``` 20 | start - 开始使用 21 | help - 帮助 22 | credits - 致谢 23 | ping - 运行状态 24 | settings - 获取公告 25 | zimuxia_online - 字幕侠在线数据 26 | newzmz_online - new字幕组在线数据 27 | yyets_offline - 人人影视离线数据 28 | ``` 29 | 30 | # 截图 31 | 32 | ## 常规搜索 33 | 34 | ![](assets/1.png) 35 | 36 | ## 资源分享站截图 37 | 38 | 本网站永久免费,并且没有任何限制。 39 | ![](assets/new_resource.png) 40 | 41 | ![](assets/2.png) 42 | 43 | 支持收藏功能,会跨设备同步 44 | ![](assets/like.png) 45 | 46 | ## 指定字幕组搜索 47 | 48 | 目前只支持YYeTsOffline、ZimuxiaOnline和NewzmzOnline 49 | 50 | ![](assets/3.png) 51 | 52 | # 如何下载磁力和电驴资源?迅雷提示资源敏感 53 | 54 | ## 电驴资源 55 | 56 | 请下载使用 [eMule](https://www.emule-project.net/home/perl/general.cgi?l=42) ,然后添加如下两个server list 57 | 58 | * [server.met](http://www.server-met.de/) 59 | * [server list for emule](https://www.emule-security.org/serverlist/) 60 | 61 | ![](assets/emule.jpeg) 62 | 速度还可以哦 63 | 64 | ## 磁力 65 | 66 | 使用百度网盘、115等离线,或使用utorrent等工具,记得更新下 [tracker list](https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt) 67 | 哦 68 | 69 | # 小白使用 70 | 71 | 想要自己留一份资源,但是又不懂编程? 没关系!目前提供两种方式,请根据自己情况选择 72 | “离线使用” 意味着可以断网使用,但是不会自动更新资源,需要手动更新数据库;“在线应用” 意味着需要有互联网才可以使用。 73 | 74 | ## 离线 完整运行包 75 | 76 | 这个版本是新的UI,拥有全部的最新功能。运行在你本地的电脑上,不依赖外界环境。 77 | [参考文档](https://github.com/tgbot-collection/YYeTsBot/blob/master/DEVELOPMENT.md#%E4%B8%80%E9%94%AE%E8%84%9A%E6%9C%AC) 78 | 79 | ## 离线 一键运行包 80 | 81 | 一键运行包。拥有比较新的UI,只不过只有最基础的搜索、查看资源的功能。使用方法步骤如下 82 | 83 | 1. 请到 [GitHub Release](https://github.com/tgbot-collection/YYeTsBot/releases) ,找最新的 `YYeTsBot 离线一键运行包` 84 | 2. windows:双击第一步下载的exe文件; macos/Linux,cd到你的目录, `chmod +x yyetsweb ; ./yyetsweb` 85 | 3. 程序会自动下载数据库并启动。等到出现启动提示时, 打开浏览器 http://127.0.0.1:8888 就可以看到熟悉的搜索界面啦! 86 | 87 | ## 在线 原生应用程序 88 | 89 | 使用tauri封装的网页。使用方法如下 90 | 91 | 1. 请到 [GitHub Release](https://github.com/tgbot-collection/YYeTsBot/releases) ,找最新的 `YYeTsBot App` 92 | 2. windows下载msi,macos下载dmg或tar.gz,Linux下载AppImage或deb(Debian based) 93 | 3. 安装后,打开App,就可以看到熟悉的搜索界面啦! 94 | 95 | # 开发 96 | 97 | ## 网站开发 98 | 99 | 如何部署、参与开发、具体API接口,可以 [参考这个文档](DEVELOPMENT.md) 100 | 101 | ## Python Library 102 | 103 | 也可以作为Python Library去调用 104 | 105 | `pip3 install yyets` 106 | 107 | ``` 108 | >>> from yyets import YYeTs 109 | >>> yy=YYeTs("逃避") 110 | [2021-09-21 19:22:32 __init__.py:54 I] Fetching 逃避可耻却有用...https://yyets.click/api/resource?id=34812 111 | [2021-09-21 19:22:33 __init__.py:54 I] Fetching 无法逃避...https://yyets.click/api/resource?id=29540 112 | [2021-09-21 19:22:35 __init__.py:54 I] Fetching 逃避者...https://yyets.click/api/resource?id=37089 113 | 114 | >>> yy.result 115 | [, , ] 116 | 117 | >>> for y in yy.result: 118 | print(y) 119 | 120 | 逃避可耻却有用 - NIGERUHA HAJIDAGA YAKUNITATSU 121 | 无法逃避 - Inescapable 122 | 逃避者 - Shirkers 123 | 124 | >>> yy.result[0].cnname 125 | '逃避可耻却有用' 126 | 127 | >>> yy.result[0].list 128 | [{'season_num': '101', 'season_cn': '单剧', 'items': {'APP': [{'ite 129 | ``` 130 | 131 | # Credits 132 | 133 | * [人人影视](http://www.zmz2019.com/) 134 | * [追新番](http://www.fanxinzhui.com/) 135 | * [FIX字幕侠](https://www.zimuxia.cn/) 136 | * [new字幕组](https://newzmz.com/) 137 | 138 | # 支持我 139 | 140 | 觉得本项目对你有帮助?你可以通过以下方式表达你的感受: 141 | 142 | * 感谢字幕组 143 | * 点一个star🌟和fork🍴 144 | * 宣传,使用,提交问题报告 145 | * 收藏[我的博客](https://dmesg.app/) 146 | * [Telegram Channel](https://t.me/mikuri520) 147 | 148 | ## 捐助 149 | 150 | * [给我买杯咖啡?](https://www.buymeacoffee.com/bennythink) 151 | * [爱发电?](https://afdian.net/@BennyThink) 152 | * [GitHub Sponsor](https://github.com/sponsors/BennyThink) 153 | * [Stripe](https://buy.stripe.com/dR67vU4p13Ox73a6oq) 154 | 155 | stripe 156 | 157 | 158 | # License 159 | 160 | [MIT](LICENSE) 161 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/3.png -------------------------------------------------------------------------------- /assets/CNY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/CNY.png -------------------------------------------------------------------------------- /assets/ci.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/ci.gz -------------------------------------------------------------------------------- /assets/emule.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/emule.jpeg -------------------------------------------------------------------------------- /assets/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/index.png -------------------------------------------------------------------------------- /assets/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/like.png -------------------------------------------------------------------------------- /assets/new_resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/assets/new_resource.png -------------------------------------------------------------------------------- /conf/yyets.dmesg.app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name yyetsdev.dmesg.app; 5 | index index.html index.htm index.php default.html default.htm default.php; 6 | 7 | 8 | location / { 9 | proxy_pass_header Server; 10 | proxy_set_header Host $http_host; 11 | proxy_redirect off; 12 | proxy_set_header X-Real-IP $remote_addr; 13 | proxy_set_header X-Scheme $scheme; 14 | proxy_pass http://yyets-web:8888; 15 | } 16 | 17 | access_log /var/log/nginx/yyetsdev.dmesg.app.log; 18 | } 19 | 20 | server { 21 | listen 443 ssl http2 ; 22 | listen [::]:443 ssl http2; 23 | server_name yyetsdev.dmesg.app; 24 | index index.html index.htm index.php default.html default.htm default.php; 25 | 26 | ssl_certificate /etc/nginx/certs/dmesg_cf_cert.pem; 27 | ssl_certificate_key /etc/nginx/certs/dmesg_cf_key.pem; 28 | 29 | ssl_session_timeout 20m; 30 | ssl_session_cache builtin:1000 shared:SSL:10m; 31 | ssl_protocols TLSv1.2 TLSv1.3; 32 | ssl_prefer_server_ciphers on; 33 | 34 | ssl_dhparam /etc/nginx/certs/dhparam.pem; 35 | ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; 36 | ssl_stapling on; 37 | ssl_stapling_verify on; 38 | 39 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 40 | add_header X-Frame-Options "SAMEORIGIN" always; 41 | add_header X-Xss-Protection "1; mode=block" always; 42 | add_header X-Content-Type-Options "nosniff" always; 43 | 44 | add_header Content-Security-Policy "default-src https: 'unsafe-inline' 'unsafe-eval' data:;"; 45 | add_header Referrer-Policy "no-referrer-when-downgrade"; 46 | 47 | 48 | 49 | location / { 50 | proxy_pass_header Server; 51 | proxy_set_header Host $http_host; 52 | proxy_redirect off; 53 | proxy_set_header X-Real-IP $remote_addr; 54 | proxy_set_header X-Scheme $scheme; 55 | proxy_pass http://yyets-web:8888; 56 | } 57 | 58 | 59 | 60 | access_log /var/log/nginx/yyetsdev.dmesg.app.log; 61 | } 62 | -------------------------------------------------------------------------------- /conf/yyets.env: -------------------------------------------------------------------------------- 1 | mongo=mongo 2 | redis=redis 3 | email_user=username 4 | email_password=passord 5 | email_host=mailhog 6 | email_port=1025 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | redis: 5 | image: redis:7-alpine 6 | restart: always 7 | logging: 8 | driver: none 9 | 10 | mongo: 11 | image: mongo:6 12 | restart: always 13 | volumes: 14 | - ./mongo_data/mongodb:/data/db 15 | command: --quiet 16 | logging: 17 | driver: none 18 | ports: 19 | - "127.0.0.1:27017:27017" 20 | 21 | meili: 22 | image: getmeili/meilisearch:v1.0.2 23 | restart: always 24 | environment: 25 | - MEILI_HTTP_PAYLOAD_SIZE_LIMIT=1073741824 #1GiB 26 | volumes: 27 | - meilisearch_data:/meili_data 28 | 29 | mysql: 30 | image: ubuntu/mysql:8.0-22.04_beta 31 | restart: unless-stopped 32 | environment: 33 | MYSQL_ROOT_PASSWORD: 'root' 34 | logging: 35 | driver: none 36 | command: "--skip-log-bin --default-authentication-plugin=mysql_native_password" 37 | 38 | bot: 39 | image: bennythink/yyetsbot 40 | depends_on: 41 | - redis 42 | - mongo 43 | restart: always 44 | env_file: 45 | - env/yyets.env 46 | 47 | web: 48 | image: bennythink/yyetsbot 49 | restart: always 50 | env_file: 51 | - env/yyets.env 52 | depends_on: 53 | - mongo 54 | - redis 55 | - mysql 56 | working_dir: /YYeTsBot/yyetsweb/ 57 | volumes: 58 | - ./subtitle_data:/YYeTsBot/yyetsweb/subtitle_data 59 | command: [ "python3","server.py","-h=0.0.0.0" ] 60 | ports: 61 | - "127.0.0.1:8888:8888" 62 | - "172.17.0.1:8888:8888" 63 | 64 | 65 | volumes: 66 | meilisearch_data: 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.3 2 | pytelegrambotapi==4.27.0 3 | beautifulsoup4==4.13.3 4 | tgbot-ping==1.0.7 5 | redis==5.2.1 6 | apscheduler==3.11.0 7 | pymongo==4.12.1 8 | tornado==6.4.2 9 | captcha==0.7.1 10 | passlib==1.7.4 11 | fakeredis==2.29.0 12 | filetype==1.2.0 13 | requests[socks] 14 | tqdm==4.67.1 15 | retry==0.9.2 16 | pymysql==1.1.1 17 | git+https://github.com/tgbot-collection/python-akismet 18 | openpyxl==3.1.5 19 | zhconv==1.4.3 20 | jinja2==3.1.6 21 | coloredlogs==15.0.1 22 | meilisearch==0.33.0 23 | pillow==11.2.1 24 | pytz==2025.2 -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function splash() { 4 | echo "本脚本会在 ${HOME}/YYeTs 部署人人影视web" 5 | echo "你确定要继续吗?输入YES确认" 6 | read -r confirm 7 | 8 | if [ "$confirm" = "YES" ]; then 9 | echo "继续安装" 10 | else 11 | echo "取消安装" 12 | exit 1 13 | fi 14 | 15 | } 16 | 17 | function prepare() { 18 | echo "[1/5] 准备中……" 19 | mkdir -p "${HOME}"/YYeTs 20 | cd "${HOME}"/YYeTs || exit 21 | } 22 | 23 | function prepare_compose() { 24 | echo "[2/5] 下载docker-compose.yml" 25 | curl -o docker-compose.yml https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/master/docker-compose.yml 26 | sed -ie '58,59d' docker-compose.yml 27 | } 28 | 29 | function import_db() { 30 | echo "[3/5] 正在准备MongoDB" 31 | docker-compose up -d mongo 32 | 33 | echo "[4/5] 正在下载并导入数据库" 34 | curl -o /tmp/yyets_mongo.gz https://yyets.click/dump/yyets_mongo.gz 35 | file /tmp/yyets_mongo.gz 36 | docker cp /tmp/yyets_mongo.gz yyets_mongo_1:/tmp 37 | # special for windows 38 | result=$(uname -a | grep "Msys") 39 | if [[ "$result" != "" ]]; then 40 | echo "docker exec yyets_mongo_1 mongorestore --gzip --archive=/tmp/yyets_mongo.gz --nsFrom "share.*" --nsTo "zimuzu.*"" >windows.bat 41 | echo "docker exec yyets_mongo_1 rm /tmp/yyets_mongo.gz" >>windows.bat 42 | cmd "/C windows.bat" 43 | rm windows.bat 44 | else 45 | docker exec yyets_mongo_1 mongorestore --gzip --archive=/tmp/yyets_mongo.gz --nsFrom "share.*" --nsTo "zimuzu.*" 46 | docker exec yyets_mongo_1 rm /tmp/yyets_mongo.gz 47 | fi 48 | 49 | rm /tmp/yyets_mongo.gz 50 | } 51 | 52 | function up() { 53 | echo "[5/5] 启动中……" 54 | docker-compose up -d 55 | echo "部署成功。您可以访问 http://IP:8888 查看" 56 | } 57 | 58 | function deploy() { 59 | splash 60 | prepare 61 | prepare_compose 62 | import_db 63 | up 64 | } 65 | 66 | function cleanup() { 67 | echo "您确认要进行清理吗?网站将会停止运行,对应的的docker image也将会被清除。输入YES确认" 68 | read -r confirm 69 | 70 | if [ "$confirm" = "YES" ]; then 71 | echo "继续清理,可能会要求您进行sudo鉴权" 72 | docker-compose -f "${HOME}"/YYeTs/docker-compose.yml down 73 | sudo rm -rf "${HOME}"/YYeTs 74 | docker rmi bennythink/yyetsbot 75 | echo "清理完成。" 76 | else 77 | echo "取消清理" 78 | exit 1 79 | fi 80 | 81 | } 82 | 83 | function upgrade() { 84 | docker pull bennythink/yyetsbot 85 | docker-compose -f "${HOME}"/YYeTs/docker-compose.yml up -d 86 | echo "更新成功" 87 | } 88 | 89 | select MENU_ITEM in "部署YYeTs" "清理YYeTs" "更新YYeTs"; do 90 | echo "准备$MENU_ITEM YYeTsWeb..." 91 | case $MENU_ITEM in 92 | "部署YYeTs") deploy ;; 93 | "清理YYeTs") cleanup ;; 94 | "更新YYeTs") upgrade ;; 95 | *) echo "无效的操作" ;; 96 | esac 97 | break 98 | done 99 | -------------------------------------------------------------------------------- /scripts/migrate_sub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - migrate_sub.py 5 | 6 | import pymongo 7 | import pymysql 8 | from pymysql.cursors import DictCursor 9 | 10 | con = pymysql.connect(host="mysql", user="root", password="root", database="yyets", charset="utf8") 11 | cur = con.cursor(cursor=DictCursor) 12 | mongo_client = pymongo.MongoClient(host="mongo") 13 | col = mongo_client["zimuzu"]["subtitle"] 14 | 15 | cur.execute("select * from subtitle") 16 | 17 | # 56134 rows 18 | for sub in cur.fetchall(): 19 | col.insert_one(sub) 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pipenv install twine --dev 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import Command, setup 13 | 14 | # Package meta-data. 15 | NAME = "yyets" 16 | DESCRIPTION = "https://yyets.click/ wrapper" 17 | URL = "https://github.com/tgbot-collection/YYeTsBot" 18 | EMAIL = "benny.think@gmail.com" 19 | AUTHOR = "BennyThink" 20 | REQUIRES_PYTHON = ">=3.6.0" 21 | VERSION = "1.0.1" 22 | 23 | # What packages are required for this module to be executed? 24 | REQUIRED = ["requests"] 25 | 26 | # What packages are optional? 27 | EXTRAS = { 28 | # 'fancy feature': ['django'], 29 | } 30 | 31 | # The rest you shouldn't have to touch too much :) 32 | # ------------------------------------------------ 33 | # Except, perhaps the License and Trove Classifiers! 34 | # If you do change the License, remember to change the Trove Classifier for that! 35 | 36 | here = os.path.abspath(os.path.dirname(__file__)) 37 | 38 | # Import the README and use it as the long-description. 39 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 40 | try: 41 | with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f: 42 | long_description = "\n" + f.read() 43 | except FileNotFoundError: 44 | long_description = DESCRIPTION 45 | 46 | # Load the package's __version__.py module as a dictionary. 47 | about = {} 48 | if not VERSION: 49 | project_slug = NAME.lower().replace("-", "_").replace(" ", "_") 50 | with open(os.path.join(here, project_slug, "__version__.py")) as f: 51 | exec(f.read(), about) 52 | else: 53 | about["__version__"] = VERSION 54 | 55 | 56 | class UploadCommand(Command): 57 | """Support setup.py upload.""" 58 | 59 | description = "Build and publish the package." 60 | user_options = [] 61 | 62 | @staticmethod 63 | def status(s): 64 | """Prints things in bold.""" 65 | print("\033[1m{0}\033[0m".format(s)) 66 | 67 | def initialize_options(self): 68 | pass 69 | 70 | def finalize_options(self): 71 | pass 72 | 73 | def run(self): 74 | try: 75 | self.status("Removing previous builds…") 76 | rmtree(os.path.join(here, "dist")) 77 | except OSError: 78 | pass 79 | 80 | self.status("Building Source and Wheel (universal) distribution…") 81 | os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) 82 | 83 | self.status("Uploading the package to PyPI via Twine…") 84 | os.system("twine upload dist/*") 85 | 86 | self.status("Pushing git tags…") 87 | os.system("git tag v{0}".format(about["__version__"])) 88 | os.system("git push --tags") 89 | 90 | sys.exit() 91 | 92 | 93 | # Where the magic happens: 94 | setup( 95 | name=NAME, 96 | version=about["__version__"], 97 | description=DESCRIPTION, 98 | long_description=long_description, 99 | long_description_content_type="text/markdown", 100 | author=AUTHOR, 101 | author_email=EMAIL, 102 | python_requires=REQUIRES_PYTHON, 103 | url=URL, 104 | # packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), 105 | # If your package is a single module, use this instead of 'packages': 106 | packages=["yyets"], 107 | # entry_points={ 108 | # 'console_scripts': ['mycli=mymodule:cli'], 109 | # }, 110 | install_requires=REQUIRED, 111 | extras_require=EXTRAS, 112 | include_package_data=True, 113 | license="MIT", 114 | classifiers=[ 115 | # Trove classifiers 116 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 117 | "License :: OSI Approved :: MIT License", 118 | "Programming Language :: Python", 119 | "Programming Language :: Python :: 3", 120 | "Programming Language :: Python :: 3.6", 121 | "Programming Language :: Python :: Implementation :: CPython", 122 | "Programming Language :: Python :: Implementation :: PyPy", 123 | ], 124 | # $ setup.py publish support. 125 | cmdclass={ 126 | "upload": UploadCommand, 127 | }, 128 | ) 129 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x2F119b4DdC0d33A1cAE392999513e8D253C9b4Db' 6 | - '0x56E743FD305c6858A4baD2893F2b2498441dF0Ce' 7 | quorum: 1 8 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/README.md: -------------------------------------------------------------------------------- 1 | # BagAndDrag 2 | Bag and Drag 3 | 4 | [original repo](https://github.com/tgbot-collection/BagAndDrag) 5 | 6 | 打包带走:-) -------------------------------------------------------------------------------- /yyets/BagAndDrag/bag.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # BagAndDrag - bag.py 5 | # 1/10/21 15:29 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import contextlib 11 | import json 12 | import logging 13 | import os 14 | import pickle 15 | import sys 16 | import time 17 | import traceback 18 | 19 | import pymysql 20 | import requests 21 | 22 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(filename)s [%(levelname)s]: %(message)s') 23 | 24 | COOKIES = os.path.join(os.path.dirname(__file__), 'cookies.dump') 25 | USERNAME = os.environ.get("USERNAME") or "321" 26 | PASSWORD = os.environ.get("PASSWORD") or "xa31sge" 27 | 28 | GET_USER = "http://www.rrys2020.com/user/login/getCurUserTopInfo" 29 | AJAX_LOGIN = "http://www.rrys2020.com/User/Login/ajaxLogin" 30 | RESOURCE = "http://www.rrys2020.com/resource/{id}" 31 | SHARE_URL = "http://www.rrys2020.com/resource/ushare" 32 | # http://got002.com/api/v1/static/resource/detail?code=9YxN91 33 | API_DATA = "http://got002.com/api/v1/static/resource/detail?code={code}" 34 | ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 35 | 36 | 37 | def save_cookies(requests_cookiejar): 38 | with open(COOKIES, 'wb') as f: 39 | pickle.dump(requests_cookiejar, f) 40 | 41 | 42 | def load_cookies(): 43 | with contextlib.suppress(Exception): 44 | with open(COOKIES, 'rb') as f: 45 | return pickle.load(f) 46 | 47 | 48 | def login(): 49 | data = {"account": USERNAME, "password": PASSWORD, "remember": 1} 50 | logging.info("login in as %s", data) 51 | r = requests.post(AJAX_LOGIN, data=data, headers={"User-Agent": ua}) 52 | resp = r.json() 53 | if resp.get('status') == 1: 54 | logging.info("Login success! %s", r.cookies) 55 | save_cookies(r.cookies) 56 | else: 57 | logging.error("Login failed! %s", resp) 58 | sys.exit(1) 59 | r.close() 60 | 61 | 62 | def is_cookie_valid() -> bool: 63 | cookie = load_cookies() 64 | r = requests.get(GET_USER, cookies=cookie, headers={"User-Agent": ua}) 65 | return r.json()['status'] == 1 66 | 67 | 68 | def insert_db(data: dict): 69 | try: 70 | # INSERT INTO resource VALUE(id,url,name,expire,data) 71 | sql = "INSERT INTO resource VALUE(%s,%s,%s,%s,%s,%s)" 72 | con = pymysql.Connect(host="127.0.0.1", user="root", password="root", database="yyets", charset="utf8mb4") 73 | cur = con.cursor() 74 | info = data["data"]["info"] 75 | id = info["id"] 76 | url = RESOURCE.format(id=id) 77 | name = '{cnname}\n{enname}\n{alias}'.format(cnname=info["cnname"], enname=info["enname"], 78 | alias=info["aliasname"]) 79 | expire = info["expire"] 80 | date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(expire))) 81 | d = json.dumps(data, ensure_ascii=False, indent=2) 82 | 83 | cur.execute(sql, (id, url, name, expire, date, d)) 84 | con.commit() 85 | except Exception as e: 86 | logging.error("insert error %s", e) 87 | logging.error(traceback.format_exc()) 88 | 89 | 90 | def insert_error(rid, tb): 91 | logging.warning("Logging error into database %s", rid) 92 | sql = "INSERT INTO failure VALUE (%s,%s)" 93 | con = pymysql.Connect(host="127.0.0.1", user="root", password="root", database="yyets", charset="utf8mb4") 94 | cur = con.cursor() 95 | cur.execute(sql, (rid, tb)) 96 | con.commit() 97 | 98 | 99 | def __load_sample(): 100 | with open("sample.json") as f: 101 | return json.load(f) 102 | 103 | 104 | if __name__ == '__main__': 105 | d = __load_sample() 106 | insert_db(d) 107 | insert_error(2331, "eeeeee") 108 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/cfkv.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # BagAndDrag - cfkv.py 5 | # 1/17/21 12:08 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import json 11 | import os 12 | 13 | import pymysql 14 | 15 | con = pymysql.Connect(host="127.0.0.1", user="root", password="root", charset="utf8mb4", database="yyets", 16 | cursorclass=pymysql.cursors.DictCursor) 17 | cur = con.cursor() 18 | 19 | SIZE = 3000 20 | cur.execute("select count(id) from resource") 21 | count = cur.fetchall()[0]["count(id)"] 22 | LIMIT = count // SIZE + 1 23 | 24 | 25 | def convert_kv(): 26 | for i in range(1, LIMIT + 1): 27 | SQL = "select id,data from resource limit %d offset %d" % (SIZE, (i - 1) * SIZE) 28 | print(SQL) 29 | cur = con.cursor() 30 | cur.execute(SQL) 31 | data = cur.fetchall() 32 | write_data = [] 33 | for datum in data: 34 | write_data.append({ 35 | "key": str(datum["id"]), # keys need to be str 36 | "value": datum['data']} 37 | ) 38 | with open(f"kv/kv_data{i - 1}.json", "w") as f: 39 | json.dump(write_data, f, ensure_ascii=False) 40 | 41 | 42 | def verify_kv_data(): 43 | files = os.listdir("kv") 44 | rows = 0 45 | for file in files: 46 | if file.startswith("kv_data"): 47 | with open(f"kv/{file}") as f: 48 | data = json.load(f) 49 | rows += len(data) 50 | print(rows, count) 51 | # assert rows == count 52 | 53 | 54 | def dump_index(): 55 | cur = con.cursor() 56 | indexes = {} 57 | cur.execute("select name, id from resource") 58 | data = cur.fetchall() 59 | for datum in data: 60 | name = datum["name"] 61 | rid = datum["id"] 62 | indexes[name] = rid 63 | with open("kv/index.json", "w") as f: 64 | write_data = [ 65 | { 66 | "key": "index", 67 | "value": json.dumps(indexes, ensure_ascii=False) 68 | } 69 | ] 70 | json.dump(write_data, f, ensure_ascii=False, indent=2) 71 | 72 | 73 | def generate_command(): 74 | files = os.listdir("kv") 75 | tpl = "wrangler kv:bulk put --namespace-id=01d666b5ebae464193998bb074f672cf {filename}" 76 | shell = [] 77 | for file in files: 78 | if file.endswith(".json"): 79 | shell.append(tpl.format(filename=file) + "\n") 80 | with open("kv/bulk.sh", "w") as f: 81 | f.writelines(shell) 82 | 83 | 84 | if __name__ == '__main__': 85 | convert_kv() 86 | verify_kv_data() 87 | dump_index() 88 | generate_command() 89 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/convert_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # BagAndDrag - convert_db.py 5 | # 1/12/21 18:24 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | # convert to mongodb and con_sqlite 11 | 12 | import json 13 | import sqlite3 14 | from typing import List 15 | 16 | import pymongo 17 | import pymysql 18 | import tqdm 19 | 20 | con_mysql = pymysql.Connect(host="127.0.0.1", user="root", password="root", charset="utf8mb4", database="yyets", 21 | cursorclass=pymysql.cursors.DictCursor 22 | ) 23 | 24 | mongo_client = pymongo.MongoClient() 25 | con_sqlite = sqlite3.connect("yyets.db") 26 | 27 | SIZE = 2000 28 | 29 | 30 | def create_sqlite_database(): 31 | sql = [""" 32 | DROP TABLE IF EXISTS resource; 33 | """, 34 | """ 35 | create table resource 36 | ( 37 | id int primary key, 38 | url varchar(255) null unique , 39 | name text null, 40 | expire int null, 41 | expire_cst varchar(255) null, 42 | data longtext null 43 | 44 | ) 45 | """ 46 | ] 47 | cur = con_sqlite.cursor() 48 | for s in sql: 49 | cur.execute(s) 50 | con_sqlite.commit() 51 | 52 | 53 | def clear_mongodb(): 54 | mongo_client.drop_database("yyets") 55 | 56 | 57 | def sqlite_insert(data: List[dict]): 58 | cur = con_sqlite.cursor() 59 | sql = "INSERT INTO resource VALUES(?,?,?,?,?,?)" 60 | 61 | cur.executemany(sql, [list(i.values()) for i in data]) 62 | con_sqlite.commit() 63 | 64 | 65 | def mongodb_insert(data: List[dict]): 66 | db = mongo_client["yyets"] 67 | col = db["resource"] 68 | # deserialize data.data 69 | inserted = [] 70 | for i in data: 71 | i["data"] = json.loads(i["data"]) 72 | inserted.append(i) 73 | col.insert_many(inserted) 74 | 75 | 76 | def main(): 77 | create_sqlite_database() 78 | clear_mongodb() 79 | 80 | mysql_cur = con_mysql.cursor() 81 | mysql_cur.execute("select count(id) from resource") 82 | count = mysql_cur.fetchall()[0]["count(id)"] 83 | 84 | mysql_cur.execute("SELECT * FROM resource") 85 | 86 | with tqdm.tqdm(total=count * 2) as pbar: 87 | while True: 88 | data = mysql_cur.fetchmany(SIZE) 89 | if data: 90 | sqlite_insert(data) 91 | pbar.update(SIZE) 92 | mongodb_insert(data) 93 | pbar.update(SIZE) 94 | else: 95 | break 96 | 97 | 98 | if __name__ == '__main__': 99 | main() 100 | con_mysql.close() 101 | con_sqlite.close() 102 | mongo_client.close() 103 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/create_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # BagAndDrag - create_db.py 5 | # 1/10/21 15:23 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import pymysql 11 | 12 | con = pymysql.Connect(host="127.0.0.1", user="root", password="root", charset="utf8mb4") 13 | 14 | sql = [ 15 | "CREATE DATABASE yyets CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;", 16 | "use yyets", 17 | """ 18 | create table resource 19 | ( 20 | id int primary key, 21 | url varchar(255) null unique , 22 | name text null, 23 | expire int null, 24 | expire_cst varchar(255) null, 25 | data longtext null 26 | 27 | )charset utf8mb4; 28 | 29 | 30 | """, 31 | 32 | """ 33 | create table failure 34 | ( 35 | id int primary key not null, 36 | traceback longtext null 37 | )charset utf8mb4; 38 | """, 39 | 40 | ] 41 | cur = con.cursor() 42 | for s in sql: 43 | cur.execute(s) 44 | con.close() 45 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/drag.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # BagAndDrag - drag.py 5 | # 1/10/21 15:38 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import argparse 11 | import logging 12 | import time 13 | import traceback 14 | from concurrent.futures import ThreadPoolExecutor 15 | 16 | import requests 17 | from tqdm import tqdm 18 | 19 | from bag import (API_DATA, SHARE_URL, insert_db, insert_error, is_cookie_valid, 20 | load_cookies, login) 21 | 22 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(filename)s [%(levelname)s]: %(message)s') 23 | s = requests.session() 24 | ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 25 | s.headers.update({"User-Agent": ua}) 26 | 27 | parser = argparse.ArgumentParser() 28 | # start_id, end_id, interval, concurrency 29 | parser.add_argument("-s", help="Start id", type=int, default=1) 30 | parser.add_argument("-e", help="End id", type=int, default=10) 31 | parser.add_argument("-i", help="Interval", default=5, type=int) 32 | parser.add_argument("-c", help="Concurrency", default=2, type=int) 33 | args = parser.parse_args() 34 | 35 | executor = ThreadPoolExecutor(max_workers=args.c) 36 | 37 | 38 | def get_api_json(resource_id): 39 | try: 40 | time.sleep(args.i) 41 | if not is_cookie_valid(): 42 | login() 43 | logging.info("resource id is %s", resource_id) 44 | res = s.post(SHARE_URL, data={"rid": resource_id}, cookies=load_cookies()).json() 45 | share_code = res['data'].split('/')[-1] 46 | logging.info("Share code is %s", share_code) 47 | data = s.get(API_DATA.format(code=share_code)).json() 48 | 49 | insert_db(data) 50 | except Exception: 51 | insert_error(resource_id, traceback.format_exc()) 52 | 53 | 54 | def main(): 55 | total = args.e + 1 - args.s 56 | list(tqdm(executor.map(get_api_json, range(args.s, args.e + 1)), total=total)) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/zimuxia/convert_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - convert_db.py 5 | # 2/5/21 13:46 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | # convert to mongodb and con_sqlite 11 | 12 | import pymongo 13 | import pymysql 14 | import tqdm 15 | import json 16 | 17 | from typing import List 18 | 19 | con_mysql = pymysql.Connect(host="127.0.0.1", user="root", password="root", charset="utf8mb4", database="zimuxia", 20 | cursorclass=pymysql.cursors.DictCursor 21 | ) 22 | 23 | mongo_client = pymongo.MongoClient() 24 | 25 | SIZE = 2000 26 | 27 | 28 | def clear_mongodb(): 29 | mongo_client.drop_database("zimuxia") 30 | 31 | 32 | def clear_mysql(): 33 | con_mysql.cursor().execute("truncate table resource;") 34 | con_mysql.commit() 35 | 36 | 37 | def mysql_insert(data: List[dict]): 38 | sql = "INSERT INTO resource VALUES(NULL,%(url)s,%(name)s,NULL,NULL,%(data)s)" 39 | cur = con_mysql.cursor() 40 | for i in data: 41 | cur.execute(sql, i) 42 | con_mysql.commit() 43 | 44 | 45 | def mongodb_insert(data: List[dict]): 46 | db = mongo_client["zimuxia"] 47 | col = db["resource"] 48 | col.insert_many(data) 49 | 50 | 51 | def main(): 52 | clear_mongodb() 53 | clear_mysql() 54 | with open("result.json") as f: 55 | data = json.load(f) 56 | # [{"url": "https://www.zimuxia.cn/portfolio/%e6%888b%e5%8f%8b", "name": "我家的女儿交不到男朋友", "data":""}] 57 | mysql_insert(data) 58 | mongodb_insert(data) 59 | 60 | 61 | if __name__ == '__main__': 62 | main() 63 | con_mysql.close() 64 | mongo_client.close() 65 | -------------------------------------------------------------------------------- /yyets/BagAndDrag/zimuxia/zimuxia.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - zimuxia.py 5 | # 2/5/21 12:44 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import requests 11 | import random 12 | import time 13 | import json 14 | from bs4 import BeautifulSoup 15 | from urllib.parse import quote, unquote 16 | import tqdm 17 | import logging 18 | 19 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(filename)s [%(levelname)s]: %(message)s') 20 | 21 | list_url = "https://www.zimuxia.cn/%e6%88%91%e4%bb%ac%e7%9a%84%e4%bd%9c%e5%93%81?set={}" 22 | ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 23 | 24 | s = requests.session() 25 | s.headers.update({"User-Agent": ua}) 26 | 27 | data = [] 28 | 29 | 30 | def get_list(): 31 | for index in tqdm.trange(1, 89): 32 | time.sleep(random.random()) 33 | url = list_url.format(index) 34 | list_html = s.get(url).text 35 | get_episode(list_html) 36 | 37 | 38 | def get_episode(html_text): 39 | soup = BeautifulSoup(html_text, 'html.parser') 40 | episodes = soup.find_all("div", class_="pg-item") 41 | 42 | for block in episodes: 43 | url = block.a['href'] 44 | name = unquote(url).split("https://www.zimuxia.cn/portfolio/")[1] 45 | logging.info("fetching %s", name) 46 | t = {"url": url, "name": name, "data": s.get(url).text} 47 | data.append(t) 48 | 49 | 50 | def write_json(): 51 | with open("result.json", "w") as f: 52 | json.dump(data, f, ensure_ascii=False, indent=4) 53 | 54 | 55 | if __name__ == '__main__': 56 | get_list() 57 | write_json() 58 | -------------------------------------------------------------------------------- /yyets/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - __init__.py 5 | # 9/21/21 18:09 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import logging 11 | 12 | import requests 13 | 14 | API = "https://yyets.click/api/resource?" 15 | 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format="[%(asctime)s %(filename)s:%(lineno)d %(levelname).1s] %(message)s", 19 | datefmt="%Y-%m-%d %H:%M:%S", 20 | ) 21 | 22 | 23 | class Resource: 24 | def __init__(self): 25 | self.enname = None 26 | self.cnname = None 27 | 28 | def __str__(self): 29 | return f"{self.cnname} - {self.enname}" 30 | 31 | 32 | class YYeTs: 33 | def __init__(self, keyword: "str"): 34 | self.result = [] 35 | self.keyword = keyword 36 | self.search_api = f"{API}keyword={self.keyword}" 37 | self.resource_api = f"{API}id=%s" 38 | self.search() 39 | 40 | def search(self): 41 | data = requests.get(self.search_api).json() 42 | for info in data["data"]: 43 | r = Resource() 44 | setattr(r, "list", self.fetch(info)) 45 | for k, v in info.items(): 46 | setattr(r, k, v) 47 | self.result.append(r) 48 | 49 | def fetch(self, info): 50 | rid = info["id"] 51 | url = self.resource_api % rid 52 | headers = {"Referer": url} 53 | logging.info("Fetching %s...%s", info["cnname"], url) 54 | return requests.get(url, headers=headers).json()["data"]["list"] 55 | 56 | def __str__(self): 57 | return f"{self.keyword} - {self.search_api}" 58 | 59 | 60 | if __name__ == "__main__": 61 | ins = YYeTs("逃避可耻") 62 | for i in ins.result: 63 | print(i) 64 | -------------------------------------------------------------------------------- /yyets/healthcheck/check.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - check.py 5 | # 1/22/21 16:36 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import logging 11 | import os 12 | 13 | import requests 14 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 15 | from telethon import TelegramClient, events 16 | 17 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(filename)s [%(levelname)s]: %(message)s") 18 | logging.getLogger("apscheduler.executors.default").propagate = False 19 | api_id = int(os.environ.get("API_ID") or "3") 20 | api_hash = os.environ.get("API_HASH") or "4" 21 | bot_name = os.environ.get("BOT_NAME") or "yyets_bot" 22 | bot_token = os.environ.get("BOT_token") or "123" 23 | 24 | client = TelegramClient( 25 | "client-hc", api_id, api_hash, device_model="Benny-health-check", system_version="89", app_version="1.0.0" 26 | ) 27 | check_status = [] 28 | 29 | 30 | @client.on(events.NewMessage(incoming=True, pattern="(?i).*欢迎使用,直接发送想要的剧集标题给我就可以了.*", from_users=bot_name)) 31 | async def my_event_handler(event): 32 | logging.info("Okay it's working %s", event) 33 | check_status.clear() 34 | 35 | 36 | async def send_health_check(): 37 | if check_status: 38 | # restart it 39 | await bot_warning() 40 | else: 41 | await client.send_message(bot_name, "/start") 42 | check_status.append("check") 43 | 44 | 45 | async def bot_warning(): 46 | logging.warning("Bot seems to be down. Restarting now....") 47 | message = "Bot is down!!!" 48 | url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id=260260121&text={message}" 49 | resp = requests.get(url).json() 50 | logging.warning(resp) 51 | 52 | 53 | async def website_check(): 54 | home = "https://yyets.click/" 55 | top = "https://yyets.click/api/top" 56 | message = "" 57 | try: 58 | resp1 = requests.get(home) 59 | resp2 = requests.get(top) 60 | except Exception as e: 61 | message += f"Website is down. Requests error:{e}\n" 62 | resp1 = resp2 = "" 63 | 64 | if getattr(resp1, "status_code", 0) != 200: 65 | content = getattr(resp1, "content", None) 66 | message += f"Website home is down!!! {content}\n" 67 | if getattr(resp2, "status_code", 0) != 200: 68 | content = getattr(resp1, "content", None) 69 | message += f"Website top is down!!! {content}\n" 70 | if message: 71 | url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id=260260121&text={message}" 72 | resp = requests.get(url).json() 73 | logging.error(resp) 74 | logging.error(message) 75 | else: 76 | logging.info("It's working home: %s bytes; top: %s bytes", len(resp1.content), len(resp2.content)) 77 | 78 | 79 | if __name__ == "__main__": 80 | scheduler = AsyncIOScheduler() 81 | scheduler.add_job(send_health_check, "interval", seconds=300) 82 | scheduler.add_job(website_check, "interval", seconds=60) 83 | scheduler.start() 84 | client.start() 85 | client.run_until_disconnected() 86 | -------------------------------------------------------------------------------- /yyets/healthcheck/restart_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # untitled - restart_service.py 5 | # 9/18/21 11:54 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import logging.handlers 11 | import subprocess 12 | 13 | import requests 14 | 15 | filename = "yyets_restart.log" 16 | formatter = logging.Formatter("[%(asctime)s %(filename)s:%(lineno)d %(levelname).1s] %(message)s", "%Y-%m-%d %H:%M:%S") 17 | handler = logging.handlers.RotatingFileHandler(filename, maxBytes=1024 * 1024 * 100) 18 | handler.setFormatter(formatter) 19 | logger = logging.getLogger() 20 | logger.addHandler(handler) 21 | logger.setLevel(logging.INFO) 22 | 23 | URL = "https://yyets.click/api/top" 24 | # URL = "https://www.baidu.com/" 25 | req = requests.get(URL) 26 | 27 | if req.status_code != 200: 28 | logger.error("error! %s", req) 29 | cmd = "/usr/bin/docker-compose -f /home/WebsiteRunner/docker-compose.yml restart mongo yyets-web nginx" 30 | subprocess.check_output(cmd.split()) 31 | else: 32 | logger.info("YYeTs is running %s", req) 33 | -------------------------------------------------------------------------------- /yyets/management/format.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "info": "OK", 4 | "data": { 5 | "info": { 6 | "id": 99999, 7 | "cnname": "中文名", 8 | "enname": "英文名", 9 | "aliasname": "别名", 10 | "channel": "movie/tv", 11 | "channel_cn": "电影/美剧", 12 | "area": "法国", 13 | "show_type": "", 14 | "expire": "1610401225", 15 | "views": 0 16 | }, 17 | "list": [ 18 | { 19 | "season_num": "0", 20 | "season_cn": "正片", 21 | "items": { 22 | "MP4": [ 23 | { 24 | "itemid": "428324", 25 | "episode": "0", 26 | "name": "name", 27 | "size": "1.43GB", 28 | "yyets_trans": 0, 29 | "dateline": "1565351112", 30 | "files": [ 31 | { 32 | "way": "1", 33 | "way_cn": "电驴", 34 | "address": "", 35 | "passwd": "" 36 | }, 37 | { 38 | "way": "2", 39 | "way_cn": "磁力", 40 | "address": "", 41 | "passwd": "" 42 | }, 43 | { 44 | "way": "9", 45 | "way_cn": "网盘", 46 | "address": "", 47 | "passwd": "" 48 | }, 49 | { 50 | "way": "115", 51 | "way_cn": "微云", 52 | "address": "", 53 | "passwd": "" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | "formats": [ 60 | "MP4" 61 | ] 62 | } 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /yyets/management/ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - ui.py 5 | # 2/8/21 17:55 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import json 11 | import time 12 | 13 | import PySimpleGUI as sg 14 | 15 | # All the stuff inside your window. 16 | channel_map = { 17 | "movie": "电影", 18 | "tv": "电视剧" 19 | } 20 | 21 | complete = { 22 | "status": 1, 23 | "info": "OK", 24 | "data": { 25 | "info": {}, 26 | "list": [ 27 | { 28 | "season_num": "1", 29 | "season_cn": "第一季", 30 | "items": {}, 31 | "formats": [ 32 | "MP4" 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | 39 | dl = { 40 | "itemid": "", 41 | "episode": "0", 42 | "name": "", 43 | "size": "", 44 | "yyets_trans": 0, 45 | "dateline": str(int(time.time())), 46 | "files": [ 47 | { 48 | "way": "1", 49 | "way_cn": "电驴", 50 | "address": "", 51 | "passwd": "" 52 | }, 53 | { 54 | "way": "2", 55 | "way_cn": "磁力", 56 | "address": "", 57 | "passwd": "" 58 | }, 59 | { 60 | "way": "9", 61 | "way_cn": "百度网盘", 62 | "address": "", 63 | "passwd": "" 64 | }, 65 | { 66 | "way": "115", 67 | "way_cn": "115网盘", 68 | "address": "", 69 | "passwd": "" 70 | } 71 | ] 72 | } 73 | item_structure = { 74 | "MP4": [ 75 | ] 76 | } 77 | 78 | 79 | def get_value(): 80 | for i in range(1, int(episode_input[1].get()) + 1): 81 | d = dl.copy() 82 | d["episode"] = str(i) 83 | d["name"] = "{}第{}集".format(cn_input[1].get(), i) 84 | item_structure["MP4"].append(d) 85 | 86 | info_structure = { 87 | "id": 0, 88 | "cnname": cn_input[1].get(), 89 | "enname": en_input[1].get(), 90 | "aliasname": alias_input[1].get(), 91 | "channel": channel_input[1].get(), 92 | "channel_cn": channel_map.get(channel_input[1].get(), ""), 93 | "area": area_input[1].get(), 94 | "show_type": "", 95 | "expire": "1610401225", 96 | "views": 0 97 | } 98 | 99 | complete["data"]["info"] = info_structure 100 | complete["data"]["list"][0]["items"] = item_structure 101 | 102 | with open("sample.json", "w") as f: 103 | json.dump(complete, f, indent=4, ensure_ascii=False) 104 | 105 | 106 | cn_input = [sg.Text('cn name'), sg.InputText()] 107 | en_input = [sg.Text('en name'), sg.InputText()] 108 | alias_input = [sg.Text('alias name'), sg.InputText()] 109 | channel_input = [sg.Text('channel'), sg.Combo(['movie', 'tv'], "tv")] 110 | area_input = [sg.Text('area'), sg.Combo(['美国', '日本', '韩国', '英国'], "美国")] 111 | episode_input = [sg.Text('episode count'), sg.InputText()] 112 | 113 | layout = [cn_input, en_input, alias_input, channel_input, area_input, episode_input, 114 | [sg.Button('Ok')] 115 | ] 116 | 117 | # Create the Window 118 | window = sg.Window('Management', layout) 119 | # Event Loop to process "events" and get the "values" of the inputs 120 | while True: 121 | event, values = window.read() 122 | if event == sg.WIN_CLOSED or event == 'Cancel': # if user closes window or clicks cancel 123 | break 124 | if event == "Ok": 125 | print('You entered ', values[0], values[1], values[2]) 126 | get_value() 127 | break 128 | window.close() 129 | -------------------------------------------------------------------------------- /yyets/worker/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/.cargo-ok -------------------------------------------------------------------------------- /yyets/worker/README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Worker部署方式 2 | **This worker is deprecated. No further updates from now on.** 3 | 4 | ## 1. 安装wrangler等工具 5 | [Cloudflare Docs](https://developers.cloudflare.com/workers/cli-wrangler) 6 | ## 2. 导入数据到MySQL 7 | 8 | ## 3. 生成KV数据 9 | 参考 `BagAndDrag/cfkv.py` 10 | 11 | ## 4. 导入数据 12 | ```shell 13 | cd kv_data 14 | bash bulk.sh 15 | ``` 16 | **注意,Cloudflare KV免费版有每日1000条写入的限制** 17 | 18 | ## 4. 配置代码 19 | * 修改`worker/public/js/search.js`中的`baseURL`为你的URL 20 | * 如绑定自定义域名,还需要修改`worker/workers-site/index.js`中的`Access-Control-Allow-Origin`为你的域名 21 | 22 | ## 5. 发布到Worker Site 23 | 配置 `wrangler.toml`,修改`account_id`, `kv_namespaces`等,然后: 24 | ```shell 25 | wrangler publish 26 | ``` 27 | 就可以了。 -------------------------------------------------------------------------------- /yyets/worker/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | 33 | 34 |
35 |

404 Not Found

36 |

Oh dang! We couldn't find that page.

37 | a sad crab is unable to unable to lasso a paper airplane. 404 not found. 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /yyets/worker/public/css/3rd/icons.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @IMPORT url("../font-awesome.min.css"); 3 | 4 | -------------------------------------------------------------------------------- /yyets/worker/public/css/3rd/widgets.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | 4 | -------------------------------------------------------------------------------- /yyets/worker/public/css/aYin.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /*webkit核心浏览器滚动条设置*/ 4 | /* 5 | ::-webkit-scrollbar{width: 10px;height:10px;} 6 | ::-webkit-scrollbar-track{border-radius: 0px;background: rgba(0,0,0,0.1);} 7 | ::-webkit-scrollbar-thumb{border-radius: 5px;background: rgba(0,0,0,0.2);} 8 | ::-webkit-scrollbar-thumb:hover{border-radius: 5px;background: rgba(0,0,0,0.4);} 9 | */ 10 | -------------------------------------------------------------------------------- /yyets/worker/public/css/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "info": "OK", 4 | "data": { 5 | "info": { 6 | "id": 10060, 7 | "cnname": "铁皮人", 8 | "enname": "Tin Man", 9 | "aliasname": "", 10 | "channel": "tv", 11 | "channel_cn": "美剧", 12 | "area": "美国", 13 | "show_type": "", 14 | "expire": "1610371223", 15 | "views": "0" 16 | }, 17 | "list": [ 18 | { 19 | "season_num": "102", 20 | "season_cn": "MINI剧", 21 | "items": { 22 | "APP": [ 23 | { 24 | "itemid": "323953", 25 | "episode": "3", 26 | "name": "yyets://N=铁皮人.Tin.Man.Part3.中英字幕.BD.720p-人人影视.mp4|S=1327396629|H=1dd3a8b0ab765f4fd77f62e15b6635b932f5f9f7|", 27 | "size": "", 28 | "yyets_trans": 0, 29 | "dateline": "1493450710", 30 | "files": [ 31 | { 32 | "way": "102", 33 | "way_cn": "百度云", 34 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 35 | "passwd": "" 36 | }, 37 | { 38 | "way": "115", 39 | "way_cn": "微云", 40 | "address": "http://url.cn/5ZsPVGj", 41 | "passwd": "" 42 | } 43 | ] 44 | }, 45 | { 46 | "itemid": "323952", 47 | "episode": "2", 48 | "name": "yyets://N=铁皮人.Tin.Man.Part2.中英字幕.BD.720p-人人影视.mp4|S=1315079304|H=68edc943d6d9c67b0727c79e9b8531170559d459|", 49 | "size": "", 50 | "yyets_trans": 0, 51 | "dateline": "1493450699", 52 | "files": [ 53 | { 54 | "way": "102", 55 | "way_cn": "百度云", 56 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 57 | "passwd": "" 58 | }, 59 | { 60 | "way": "115", 61 | "way_cn": "微云", 62 | "address": "http://url.cn/5ZsPVGj", 63 | "passwd": "" 64 | } 65 | ] 66 | }, 67 | { 68 | "itemid": "323951", 69 | "episode": "1", 70 | "name": "yyets://N=铁皮人.Tin.Man.Part1.中英字幕.BD.720p-人人影视.mp4|S=1372733541|H=657ad3f2efd48057f5bea2e0ab111d9f429a785c|", 71 | "size": "", 72 | "yyets_trans": 0, 73 | "dateline": "1493450684", 74 | "files": [ 75 | { 76 | "way": "102", 77 | "way_cn": "百度云", 78 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 79 | "passwd": "" 80 | }, 81 | { 82 | "way": "115", 83 | "way_cn": "微云", 84 | "address": "http://url.cn/5ZsPVGj", 85 | "passwd": "" 86 | } 87 | ] 88 | } 89 | ], 90 | "MP4": [ 91 | { 92 | "itemid": "429977", 93 | "episode": "3", 94 | "name": "铁皮人.Tin.Man.Part3.中英字幕.BD.720p-人人影视.mp4", 95 | "size": "1.24GB", 96 | "yyets_trans": 0, 97 | "dateline": "1566559500", 98 | "files": [ 99 | { 100 | "way": "1", 101 | "way_cn": "电驴", 102 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part3.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1327396629|23b315a8d9e4a3f711494ccae4fc766e|h=mccx743p3l67dy7fqsrrqfhu4i6q3ohs|/", 103 | "passwd": "" 104 | }, 105 | { 106 | "way": "2", 107 | "way_cn": "磁力", 108 | "address": "magnet:?xt=urn:btih:fd23ad303b7121f91f56ecfd09faf0595a83c63e&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 109 | "passwd": "" 110 | }, 111 | { 112 | "way": "12", 113 | "way_cn": "诚通网盘", 114 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394367733", 115 | "passwd": "" 116 | } 117 | ] 118 | }, 119 | { 120 | "itemid": "429976", 121 | "episode": "2", 122 | "name": "铁皮人.Tin.Man.Part2.中英字幕.BD.720p-人人影视.mp4", 123 | "size": "1.22GB", 124 | "yyets_trans": 0, 125 | "dateline": "1566559500", 126 | "files": [ 127 | { 128 | "way": "1", 129 | "way_cn": "电驴", 130 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part2.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1315079304|7c602e1f24fd9f1fa65924eee0c4fd7d|h=mzg633nkorz7kmq53z2ecccnkchdle4m|/", 131 | "passwd": "" 132 | }, 133 | { 134 | "way": "2", 135 | "way_cn": "磁力", 136 | "address": "magnet:?xt=urn:btih:a40429e64c8e1a300f502045ddda8c74d45bd6a0&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 137 | "passwd": "" 138 | }, 139 | { 140 | "way": "12", 141 | "way_cn": "诚通网盘", 142 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394335534", 143 | "passwd": "" 144 | } 145 | ] 146 | }, 147 | { 148 | "itemid": "429975", 149 | "episode": "1", 150 | "name": "铁皮人.Tin.Man.Part1.中英字幕.BD.720p-人人影视.mp4", 151 | "size": "1.28GB", 152 | "yyets_trans": 0, 153 | "dateline": "1566559500", 154 | "files": [ 155 | { 156 | "way": "1", 157 | "way_cn": "电驴", 158 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part1.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1372733541|58587c085040239c982ab8d7ff485515|h=nbd6y4jgdnwmh6ezarm2cfxiy5aw3qtp|/", 159 | "passwd": "" 160 | }, 161 | { 162 | "way": "2", 163 | "way_cn": "磁力", 164 | "address": "magnet:?xt=urn:btih:f2798104a57a380c0db44940ed180fad16930ede&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 165 | "passwd": "" 166 | }, 167 | { 168 | "way": "12", 169 | "way_cn": "诚通网盘", 170 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394335532", 171 | "passwd": "" 172 | } 173 | ] 174 | } 175 | ] 176 | }, 177 | "formats": [ 178 | "APP", 179 | "MP4" 180 | ] 181 | } 182 | ] 183 | } 184 | } -------------------------------------------------------------------------------- /yyets/worker/public/css/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "越狱\nPrison Break\n逃": 10004, 3 | "死亡地带\nThe Dead Zone\n": 10010, 4 | "伪装者\nThe Pretender\n": 10017, 5 | "橘子郡男孩\nTHE OC\nThe.O.C / 橘镇风云 /桔子镇": 10022, 6 | "威尔和格蕾丝\nWill and Grace\n威尔与格蕾丝": 10026, 7 | "黑锅\nTraveler\n逃亡之旅": 10052, 8 | "棕榈泉疑云\nHidden Palms\n": 10053, 9 | "降世神通 最后的气宗\nAvatar: The Last Airbender\n": 10054, 10 | "恐怖大师\nMasters Of Horror\n": 10055, 11 | "时间旅人\nJourneyman\n时间旅者": 10057 12 | } -------------------------------------------------------------------------------- /yyets/worker/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/favicon.ico -------------------------------------------------------------------------------- /yyets/worker/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /yyets/worker/public/fonts/test.txt: -------------------------------------------------------------------------------- 1 | 1234 -------------------------------------------------------------------------------- /yyets/worker/public/img/11bcd4d0f2daf8b02fecc72bc8ca38ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/img/11bcd4d0f2daf8b02fecc72bc8ca38ab.png -------------------------------------------------------------------------------- /yyets/worker/public/img/200-wrangler-ferris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/img/200-wrangler-ferris.gif -------------------------------------------------------------------------------- /yyets/worker/public/img/404-wrangler-ferris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/img/404-wrangler-ferris.gif -------------------------------------------------------------------------------- /yyets/worker/public/img/grid16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/img/grid16.png -------------------------------------------------------------------------------- /yyets/worker/public/img/yyetsTrans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyets/worker/public/img/yyetsTrans.png -------------------------------------------------------------------------------- /yyets/worker/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 人人影视下载分享 6 | 7 | 8 | 9 | 10 | 35 | 36 | 37 |
38 |

人人影视下载分享

39 |

By Benny

40 |
41 |

这是我偷过来的,嘿嘿😝

42 |

这个 Telegram Bot 43 | 里去使用,或者使用下方搜索框

44 |
45 | 48 | 49 | 50 |
51 |

有问题联系 Benny小可爱

52 | a happy crab is wearing a cowboy hat and holding a lasso. 200 success. 54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /yyets/worker/public/js/jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using UglifyJS v3.4.4. 3 | * Original file: /npm/jquery-mousewheel@3.1.13/jquery.mousewheel.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(d){var c,m,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in document||9<=document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],g=Array.prototype.slice;if(d.event.fixHooks)for(var i=e.length;i;)d.event.fixHooks[e[--i]]=d.event.mouseHooks;var w=d.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],n,!1);else this.onmousewheel=n;d.data(this,"mousewheel-line-height",w.getLineHeight(this)),d.data(this,"mousewheel-page-height",w.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],n,!1);else this.onmousewheel=null;d.removeData(this,"mousewheel-line-height"),d.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=d(e),i=t["offsetParent"in d.fn?"offsetParent":"parent"]();return i.length||(i=d("body")),parseInt(i.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return d(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function n(e){var t,i=e||window.event,n=g.call(arguments,1),o=0,l=0,s=0,a=0,h=0;if((e=d.event.fix(i)).type="mousewheel","detail"in i&&(s=-1*i.detail),"wheelDelta"in i&&(s=i.wheelDelta),"wheelDeltaY"in i&&(s=i.wheelDeltaY),"wheelDeltaX"in i&&(l=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in i&&(o=s=-1*i.deltaY),"deltaX"in i&&(l=i.deltaX,0===s&&(o=-1*l)),0!==s||0!==l){if(1===i.deltaMode){var r=d.data(this,"mousewheel-line-height");o*=r,s*=r,l*=r}else if(2===i.deltaMode){var u=d.data(this,"mousewheel-page-height");o*=u,s*=u,l*=u}if(t=Math.max(Math.abs(s),Math.abs(l)),(!m||t${name}`; 44 | let backup = div.innerHTML; 45 | div.innerHTML = backup + html; 46 | } 47 | } 48 | if (found === false) { 49 | div.innerHTML = `

没有搜索到结果 (ノへ ̄、)

` 50 | } 51 | } 52 | 53 | function reloadIndex() { 54 | loadJSON(indexURL, 55 | function (data) { 56 | let jsonText = JSON.stringify(data); 57 | localStorage.setItem("index", jsonText); 58 | 59 | }, 60 | function (xhr) { 61 | console.error(xhr); 62 | }); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /yyets/worker/public/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 人人影视下载分享 6 | 7 | 8 | 9 | 32 | 33 | 34 |
35 |
36 |

人人影视下载分享

37 |

By Benny

38 |
39 |

这是我偷过来的,嘿嘿😝

40 |

这个 Telegram Bot 41 | 里去使用,或者使用下方搜索框

42 |
43 | 46 | 47 | 48 |
49 | 50 |
51 |
52 |
53 |
54 |

有问题请联系 Benny小可爱

55 |
56 | 57 | 58 | 82 | 83 | -------------------------------------------------------------------------------- /yyets/worker/workers-site/index.js: -------------------------------------------------------------------------------- 1 | import {getAssetFromKV} from '@cloudflare/kv-asset-handler' 2 | 3 | /** 4 | * The DEBUG flag will do two things that help during development: 5 | * 1. we will skip caching on the edge, which makes it easier to 6 | * debug. 7 | * 2. we will return an error message on exception in your Response rather 8 | * than the default 404.html page. 9 | */ 10 | const DEBUG = false 11 | 12 | addEventListener('fetch', event => { 13 | try { 14 | event.respondWith(handleEvent(event)) 15 | } catch (e) { 16 | if (DEBUG) { 17 | return event.respondWith( 18 | new Response(e.message || e.toString(), { 19 | status: 500, 20 | }), 21 | ) 22 | } 23 | event.respondWith(new Response('Internal Error', {status: 500})) 24 | } 25 | }) 26 | 27 | async function handleEvent(event) { 28 | const url = event.request.url 29 | let options = {} 30 | let resourceId = url.split("id=")[1]; 31 | // we have valid id=10004 and the page is not resource.html 32 | if (resourceId !== undefined && url.indexOf("resource.html") === -1) { 33 | const value = await yyets.get(resourceId) 34 | if (value === null) { 35 | return new Response("resource not found", {status: 404}) 36 | } 37 | 38 | if (resourceId !== "index") { 39 | // be aware of 1000 put for free tier 40 | await update_downloads(value) 41 | } 42 | 43 | return new Response(value, { 44 | headers: { 45 | "content-type": "application/json;charset=UTF-8", 46 | "Access-Control-Allow-Origin": "https://yyets.click" 47 | }, 48 | }) 49 | 50 | } 51 | 52 | /** 53 | * You can add custom logic to how we fetch your assets 54 | * by configuring the function `mapRequestToAsset` 55 | */ 56 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 57 | 58 | try { 59 | if (DEBUG) { 60 | // customize caching 61 | options.cacheControl = { 62 | bypassCache: true, 63 | } 64 | } 65 | 66 | const page = await getAssetFromKV(event, options) 67 | 68 | // allow headers to be altered 69 | const response = new Response(page.body, page) 70 | 71 | response.headers.set('X-XSS-Protection', '1; mode=block') 72 | response.headers.set('X-Content-Type-Options', 'nosniff') 73 | response.headers.set('X-Frame-Options', 'DENY') 74 | response.headers.set('Referrer-Policy', 'unsafe-url') 75 | response.headers.set('Feature-Policy', 'none') 76 | 77 | return response 78 | 79 | } catch (e) { 80 | // if an error is thrown try to serve the asset at 404.html 81 | if (!DEBUG) { 82 | try { 83 | let notFoundResponse = await getAssetFromKV(event, { 84 | mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req), 85 | }) 86 | 87 | return new Response(notFoundResponse.body, {...notFoundResponse, status: 404}) 88 | } catch (e) { 89 | } 90 | } 91 | 92 | return new Response(e.message || e.toString(), {status: 500}) 93 | } 94 | } 95 | 96 | async function update_downloads(value) { 97 | // value is string 98 | let obj = JSON.parse(value); 99 | let targetID = obj.data.info.id.toString(); 100 | let intView = parseInt(obj.data.info.views) 101 | obj.data.info.views = (intView + 1).toString() 102 | 103 | let increasedData = JSON.stringify(obj) 104 | await yyets.put(targetID, increasedData) 105 | } 106 | -------------------------------------------------------------------------------- /yyets/worker/workers-site/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workers-site", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@cloudflare/kv-asset-handler": "~0.0.11" 12 | } 13 | }, 14 | "node_modules/@cloudflare/kv-asset-handler": { 15 | "version": "0.0.11", 16 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.0.11.tgz", 17 | "integrity": "sha512-D2kGr8NF2Er//Mx0c4+8FtOHuLrnwOlpC48TbtyxRSegG/Js15OKoqxxlG9BMUj3V/YSqtN8bUU6pjaRlsoSqg==", 18 | "dependencies": { 19 | "@cloudflare/workers-types": "^2.0.0", 20 | "@types/mime": "^2.0.2", 21 | "mime": "^2.4.6" 22 | } 23 | }, 24 | "node_modules/@cloudflare/workers-types": { 25 | "version": "2.0.0", 26 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-2.0.0.tgz", 27 | "integrity": "sha512-SFUPQzR5aV2TBLP4Re+xNX5KfAGArcRGA44OLulBDnfblEf3J+6kFvdJAQwFhFpqru3wImwT1cX0wahk6EeWTw==" 28 | }, 29 | "node_modules/@types/mime": { 30 | "version": "2.0.2", 31 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", 32 | "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" 33 | }, 34 | "node_modules/mime": { 35 | "version": "2.4.6", 36 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 37 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", 38 | "bin": { 39 | "mime": "cli.js" 40 | }, 41 | "engines": { 42 | "node": ">=4.0.0" 43 | } 44 | } 45 | }, 46 | "dependencies": { 47 | "@cloudflare/kv-asset-handler": { 48 | "version": "0.0.11", 49 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.0.11.tgz", 50 | "integrity": "sha512-D2kGr8NF2Er//Mx0c4+8FtOHuLrnwOlpC48TbtyxRSegG/Js15OKoqxxlG9BMUj3V/YSqtN8bUU6pjaRlsoSqg==", 51 | "requires": { 52 | "@cloudflare/workers-types": "^2.0.0", 53 | "@types/mime": "^2.0.2", 54 | "mime": "^2.4.6" 55 | } 56 | }, 57 | "@cloudflare/workers-types": { 58 | "version": "2.0.0", 59 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-2.0.0.tgz", 60 | "integrity": "sha512-SFUPQzR5aV2TBLP4Re+xNX5KfAGArcRGA44OLulBDnfblEf3J+6kFvdJAQwFhFpqru3wImwT1cX0wahk6EeWTw==" 61 | }, 62 | "@types/mime": { 63 | "version": "2.0.2", 64 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", 65 | "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" 66 | }, 67 | "mime": { 68 | "version": "2.4.6", 69 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 70 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /yyets/worker/workers-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "1.0.0", 4 | "description": "A template for kick starting a Cloudflare Workers project", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@cloudflare/kv-asset-handler": "~0.0.11" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /yyets/worker/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "yyets" 2 | type = "webpack" 3 | account_id = "e8d3ba82fe9e9a41cceb0047c2a2ab4f" 4 | workers_dev = true 5 | route = "" 6 | zone_id = "" 7 | kv_namespaces = [ 8 | { binding = "yyets", id = "221f4a96954b4e0da7c5a3d71b486051", preview_id = "c751ad546c5e4ff4a0e902334d75cddb" } 9 | ] 10 | 11 | [site] 12 | bucket = "./public" 13 | -------------------------------------------------------------------------------- /yyetsbot/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # YYeTsBot - config.py 3 | # 2019/8/15 18:42 4 | 5 | __author__ = "Benny " 6 | 7 | import os 8 | 9 | # website config 10 | # yyets 11 | BASE_URL = "http://www.rrys2020.com" 12 | LOGIN_URL = "http://www.rrys2020.com/user/login" 13 | GET_USER = "http://www.rrys2020.com/user/login/getCurUserTopInfo" 14 | YYETS_SEARCH_URL = "http://www.rrys2020.com/search?keyword={kw}&type=resource" 15 | AJAX_LOGIN = "http://www.rrys2020.com/User/Login/ajaxLogin" 16 | SHARE_URL = "http://www.rrys2020.com/resource/ushare" 17 | SHARE_WEB = "http://got002.com/resource.html?code={code}" 18 | # http://got002.com/api/v1/static/resource/detail?code=9YxN91 19 | SHARE_API = "http://got002.com/api/v1/static/resource/detail?code={code}" 20 | 21 | # fix zimuxia 22 | FIX_RESOURCE = "http://www.zimuxia.cn/portfolio/{name}" 23 | FIX_SEARCH = "http://www.zimuxia.cn/?s={kw}" 24 | 25 | # zhuixinfan 26 | ZHUIXINFAN_SEARCH = "http://www.fanxinzhui.com/list?k={}" 27 | ZHUIXINFAN_RESOURCE = "http://www.fanxinzhui.com{}" 28 | # yyets website 29 | DOMAIN = "https://yyets.click/" 30 | WORKERS = f"{DOMAIN}resource?id=" + "{}" 31 | # https://yyets.click/discuss#6464d5b1b27861fa44647e7e 32 | DISCUSS = f"{DOMAIN}discuss#" + "{}" 33 | # new zmz 34 | NEWZMZ_SEARCH = "https://newzmz.com/subres/index/getres.html?keyword={}" 35 | NEWZMZ_RESOURCE = "https://ysfx.tv/view/{}" 36 | 37 | # BD2020 38 | BD2020_SEARCH = "https://v.bd2020.me/search.jspx?q={}" 39 | 40 | # XL720 41 | XL720_SEARCH = "https://www.xl720.com/?s={}" 42 | 43 | # authentication config 44 | TOKEN = os.getenv("TOKEN") 45 | 46 | # network and server config 47 | PROXY = os.getenv("PROXY") 48 | REDIS = os.getenv("REDIS", "redis") 49 | MONGO = os.getenv("MONGO", "mongo") 50 | 51 | # other 52 | MAINTAINER = os.getenv("OWNER") 53 | REPORT = os.getenv("REPORT", False) 54 | # This name must match class name, other wise this bot won't running. 55 | FANSUB_ORDER: str = os.getenv("ORDER") or "YYeTsOffline,ZimuxiaOnline,NewzmzOnline,ZhuixinfanOnline,XL720,BD2020" 56 | -------------------------------------------------------------------------------- /yyetsbot/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # YYeTsBot - utils.py 3 | # 2019/8/15 20:27 4 | 5 | __author__ = 'Benny ' 6 | 7 | import pathlib 8 | 9 | import redis 10 | 11 | from config import REDIS 12 | 13 | r = redis.StrictRedis(host=REDIS, decode_responses=True) 14 | 15 | cookie_file = pathlib.Path(__file__).parent / 'data' / 'cookies.dump' 16 | 17 | 18 | def save_error_dump(uid, err: str): 19 | r.set(uid, err) 20 | 21 | 22 | def get_error_dump(uid) -> str: 23 | err = r.get(uid) 24 | r.delete(uid) 25 | if not err: 26 | err = "" 27 | return err 28 | 29 | 30 | def redis_announcement(content="", op="get"): 31 | if op == "get": 32 | return r.get("announcement") 33 | elif op == "set": 34 | r.set("announcement", content) 35 | elif op == "del": 36 | r.delete("announcement") 37 | 38 | 39 | def today_request(request_type: str): 40 | if r.exists("usage"): 41 | dict_data: dict = r.hgetall("usage") 42 | dict_data[request_type] = int(dict_data[request_type]) + 1 43 | else: 44 | data_format: dict = dict(total=0, invalid=0, answer=0, success=0, fail=0) 45 | data_format[request_type] += 1 46 | dict_data = data_format 47 | 48 | r.hset("usage", mapping=dict_data) 49 | 50 | 51 | def reset_request(): 52 | r.delete("usage") 53 | 54 | 55 | def show_usage(): 56 | m = "今天我已经服务了{total}次🤓,无效请求{invalid}😆,主人回复{answer}次🤨,成功请求{success}次😝,失败请求{fail}次🤣" 57 | if r.exists("usage"): 58 | dict_data: dict = r.hgetall("usage") 59 | else: 60 | dict_data: dict = dict(total=0, invalid=0, answer=0, success=0, fail=0) 61 | 62 | return m.format(**dict_data) 63 | -------------------------------------------------------------------------------- /yyetsbot/warning.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsbot/warning.webp -------------------------------------------------------------------------------- /yyetsweb/README.md: -------------------------------------------------------------------------------- 1 | # web端 2 | 3 | **注意:源代码中包含Google Analytics分析代码,`index.html`, `search.html`和`resource.html`。如果自己使用,要记得去除哦** 4 | 5 | # requirements 6 | 7 | * tornado 8 | * mongodb 9 | * pymongo 10 | 11 | # 导入数据 12 | 13 | 从 [这里](https://yyets.click/database) 下载mongodb数据,然后导入 14 | 15 | ```shell 16 | mongorestore --gzip --archive=yyets_mongo.gz --nsFrom "share.*" --nsTo "zimuzu.*" 17 | ``` 18 | 19 | # 运行 20 | 21 | `python server.py` 22 | 23 | # Docker 24 | 25 | 参考[这里](https://github.com/BennyThink/WebsiteRunner) 26 | 27 | # MongoDB index 28 | 29 | ```shell 30 | use zimuzu; 31 | 32 | db.getCollection('yyets').createIndex({"data.info.id": 1}); 33 | db.getCollection('yyets').createIndex({"data.info.views" : -1}); 34 | db.getCollection('yyets').createIndex({"data.info.area" : 1}); 35 | db.getCollection('yyets').getIndexes(); 36 | 37 | db.getCollection('douban').createIndex({"resourceId" : 1}); 38 | db.getCollection('douban').getIndexes(); 39 | 40 | db.getCollection('users').createIndex({"username" : 1}, { unique: true }); 41 | db.getCollection('users').createIndex( 42 | { "email.address": 1 }, 43 | { unique: true, partialFilterExpression: { "email.address": { $exists: true } } } 44 | ) 45 | db.getCollection('users').getIndexes(); 46 | 47 | db.getCollection('comment').createIndex({"resource_id" : 1}); 48 | db.getCollection('comment').getIndexes(); 49 | 50 | db.getCollection('reactions').createIndex({"comment_id" : 1}); 51 | db.getCollection('reactions').getIndexes(); 52 | 53 | db.getCollection('metrics').createIndex({"date" : 1}); 54 | db.getCollection('metrics').getIndexes(); 55 | 56 | db.getCollection('notification').createIndex({"username" : 1}); 57 | db.getCollection('notification').getIndexes(); 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /yyetsweb/commands/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - common.py 5 | # 3/26/22 10:40 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import os 11 | 12 | import pymongo 13 | 14 | 15 | class Mongo: 16 | def __init__(self): 17 | self.client = pymongo.MongoClient( 18 | host=os.getenv("MONGO", "localhost"), 19 | connect=False, 20 | connectTimeoutMS=5000, 21 | serverSelectionTimeoutMS=5000, 22 | ) 23 | self.db = self.client["zimuzu"] 24 | 25 | def __del__(self): 26 | self.client.close() 27 | -------------------------------------------------------------------------------- /yyetsweb/commands/douban_fix.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - douban_fix.py 5 | # 7/11/21 09:37 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import argparse 11 | import pathlib 12 | import sys 13 | 14 | import requests 15 | 16 | lib_path = pathlib.Path(__file__).parent.parent.resolve().as_posix() 17 | sys.path.append(lib_path) 18 | 19 | from databases.douban import Douban 20 | 21 | parser = argparse.ArgumentParser(description="豆瓣数据修复") 22 | parser.add_argument("resource_id", metavar="r", type=int, help="resource id") 23 | parser.add_argument("douban_id", metavar="d", type=int, help="douban id") 24 | args = parser.parse_args() 25 | resource_id = args.resource_id 26 | douban_id = args.douban_id 27 | 28 | douban = Douban() 29 | session = requests.Session() 30 | ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 31 | session.headers.update({"User-Agent": ua}) 32 | 33 | yyets_data = douban.db["yyets"].find_one({"data.info.id": resource_id}) 34 | search_html = "" 35 | cname = yyets_data["data"]["info"]["cnname"] 36 | 37 | final_data = douban.get_craw_data(cname, douban_id, resource_id, search_html, session) 38 | douban.db["douban"].find_one_and_replace({"resourceId": resource_id}, final_data) 39 | print("fix complete") 40 | sys.exit(0) 41 | -------------------------------------------------------------------------------- /yyetsweb/commands/grafana_test_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - grafana_test_data.py 5 | # 3/14/21 18:25 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import random 11 | from datetime import date, timedelta 12 | 13 | from common import Mongo 14 | 15 | col = Mongo().client["zimuzu"]["metrics"] 16 | 17 | 18 | def generate_date_series(start: str, end: str) -> list: 19 | start_int = [int(i) for i in start.split("-")] 20 | end_int = [int(i) for i in end.split("-")] 21 | sdate = date(*start_int) # start date 22 | edate = date(*end_int) # end date 23 | 24 | delta = edate - sdate # as timedelta 25 | days = [] 26 | for i in range(delta.days + 1): 27 | day = sdate + timedelta(days=i) 28 | days.append(day.strftime("%Y-%m-%d")) 29 | return days 30 | 31 | 32 | date_series = generate_date_series("2021-02-01", "2021-03-14") 33 | 34 | inserted = [] 35 | for date in date_series: 36 | inserted.append( 37 | { 38 | "date": date, 39 | "access": random.randint(1, 50), 40 | "search": random.randint(1, 50), 41 | "resource": random.randint(1, 50), 42 | } 43 | ) 44 | 45 | col.insert_many(inserted) 46 | -------------------------------------------------------------------------------- /yyetsweb/commands/share_excel.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - share_excel.py 5 | # 12/18/21 19:21 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import pathlib 11 | import sys 12 | 13 | import openpyxl 14 | 15 | web_path = pathlib.Path(__file__).parent.parent.resolve().as_posix() 16 | sys.path.append(web_path) 17 | from tqdm import tqdm 18 | 19 | from common.utils import ts_date 20 | from databases.base import Mongo 21 | 22 | wb = openpyxl.open("aliyun.xlsx") 23 | 24 | data = {} 25 | 26 | for ws in wb.worksheets: 27 | line = 0 28 | for line in range(1, ws.max_row + 1): 29 | name = ws.cell(line, 1).value 30 | link = ws.cell(line, 2).value 31 | line += 1 32 | data[name] = link 33 | 34 | template = { 35 | "username": "Benny", 36 | "ip": "127.0.0.1", 37 | "date": "", 38 | "browser": "cli", 39 | "content": "", 40 | "resource_id": 234, 41 | "type": "parent", 42 | } 43 | col = Mongo().db["comment"] 44 | share_doc = { 45 | "status": 1.0, 46 | "info": "OK", 47 | "data": { 48 | "info": { 49 | "id": 234, 50 | "cnname": "网友分享", 51 | "enname": "", 52 | "aliasname": "", 53 | "channel": "share", 54 | "channel_cn": "", 55 | "area": "", 56 | "show_type": "", 57 | "expire": "1610401225", 58 | "views": 0, 59 | }, 60 | "list": [], 61 | }, 62 | } 63 | 64 | Mongo().db["yyets"].update_one({"data.info.id": 234}, {"$set": share_doc}, upsert=True) 65 | 66 | for name, link in tqdm(data.items()): 67 | template["content"] = f"{name}\n{link}" 68 | template["date"] = ts_date() 69 | col.insert_one(template.copy()) 70 | -------------------------------------------------------------------------------- /yyetsweb/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - __init__.py 5 | # 2023-03-17 18:57 6 | 7 | from common.utils import setup_logger 8 | 9 | setup_logger() 10 | -------------------------------------------------------------------------------- /yyetsweb/common/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - utils.py 5 | # 6/16/21 21:42 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import contextlib 11 | import json 12 | import logging 13 | import os 14 | import pathlib 15 | import re 16 | import smtplib 17 | import time 18 | from datetime import datetime 19 | from email.header import Header 20 | from email.mime.text import MIMEText 21 | from email.utils import formataddr, parseaddr 22 | from hashlib import sha256 23 | 24 | import coloredlogs 25 | import pytz 26 | import requests 27 | from akismet import Akismet 28 | from jinja2 import Template 29 | 30 | from databases.base import Redis 31 | 32 | 33 | def setup_logger(): 34 | coloredlogs.install( 35 | level=logging.INFO, 36 | fmt="[%(asctime)s %(filename)s:%(lineno)d %(levelname).1s] %(message)s", 37 | datefmt="%Y-%m-%d %H:%M:%S", 38 | ) 39 | 40 | 41 | def hide_phone(data: list): 42 | for item in data: 43 | if item["username"].isdigit() and len(item["username"]) == 11: 44 | item["hash"] = sha256(item["username"].encode("u8")).hexdigest() 45 | item["username"] = mask_phone(item["username"]) 46 | return data 47 | 48 | 49 | def mask_phone(num): 50 | return re.sub(r"(\d{3})\d{4}(\d{4})", r"\g<1>****\g<2>", num) 51 | 52 | 53 | def ts_date(ts=None): 54 | # Let's always set the timezone to CST 55 | timestamp = ts or time.time() 56 | return datetime.fromtimestamp(timestamp, pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S") 57 | 58 | 59 | def _format_addr(s): 60 | name, addr = parseaddr(s) 61 | return formataddr((Header(name, "utf-8").encode(), addr)) 62 | 63 | 64 | def generate_body(context): 65 | template = pathlib.Path(__file__).parent.parent.joinpath("templates", "email_template.html") 66 | with open(template) as f: 67 | return Template(f.read()).render(**context) 68 | 69 | 70 | def send_mail(to: str, subject: str, context: dict): 71 | user = os.getenv("email_user") 72 | password = os.getenv("email_password") 73 | host = os.getenv("email_host", "localhost") 74 | port = os.getenv("email_port", "1025") # mailhog 75 | from_addr = os.getenv("from_addr", "hello@yyets.click") 76 | 77 | msg = MIMEText(generate_body(context), "html", "utf-8") 78 | msg["From"] = _format_addr("YYeTs <%s>" % from_addr) 79 | msg["To"] = _format_addr(to) 80 | msg["Subject"] = Header(subject, "utf-8").encode() 81 | 82 | logging.info("logging to mail server...") 83 | if port == "1025": 84 | server = smtplib.SMTP(host, int(port)) 85 | else: 86 | server = smtplib.SMTP_SSL(host, int(port)) 87 | server.login(user, password) 88 | logging.info("sending email to %s", to) 89 | server.sendmail(from_addr, [to], msg.as_string()) 90 | server.quit() 91 | 92 | 93 | def check_spam(ip, ua, author, content) -> int: 94 | # 0 means okay 95 | token = os.getenv("askismet") 96 | whitelist: "list" = os.getenv("whitelist", "").split(",") 97 | if author in whitelist: 98 | return 0 99 | if token: 100 | with contextlib.suppress(Exception): 101 | akismet = Akismet(token, blog="https://yyets.click/") 102 | 103 | return akismet.check( 104 | ip, 105 | ua, 106 | comment_author=author, 107 | blog_lang="zh_cn", 108 | comment_type="comment", 109 | comment_content=content, 110 | ) 111 | return 0 112 | 113 | 114 | class Cloudflare(Redis): 115 | key = "cf-blacklist-ip" 116 | expire = "cf-expire" 117 | 118 | def __init__(self): 119 | self.account_id = "e8d3ba82fe9e9a41cceb0047c2a2ab4f" 120 | self.item_id = "3740654e0b104053b3e5d0a71fe87b33" 121 | self.endpoint = "https://api.cloudflare.com/client/v4/accounts/{}/rules/lists/{}/items".format( 122 | self.account_id, self.item_id 123 | ) 124 | self.session = requests.Session() 125 | self.session.headers.update({"Authorization": "Bearer %s" % os.getenv("CF_TOKEN")}) 126 | super().__init__() 127 | 128 | def get_old_ips(self) -> dict: 129 | cache = self.r.get(self.key) 130 | if cache: 131 | cache = json.loads(cache) 132 | logging.info("Cache found with %s IPs", len(cache)) 133 | if len(cache) > 10000: 134 | return cache[:5000] 135 | return cache 136 | else: 137 | data = self.session.get(self.endpoint).json() 138 | result = data.get("result", []) 139 | cursor = data.get("result_info", {}).get("cursors", {}).get("after") 140 | while cursor: 141 | logging.info("Fetching next page with cursor %s", cursor) 142 | data = self.session.get(self.endpoint, params={"cursor": cursor}).json() 143 | result.extend(data["result"]) 144 | cursor = data.get("result_info", {}).get("cursors", {}).get("after") 145 | logging.info("Got %s IPs", len(result)) 146 | return result 147 | 148 | def ban_new_ip(self, ip): 149 | if ":" in ip: 150 | ip = ip.rsplit(":", 4)[0] + "::/64" 151 | old_ips = [d["ip"] for d in self.get_old_ips()] 152 | old_ips.append(ip) 153 | body = [{"ip": i} for i in set(old_ips)] 154 | self.r.set(self.key, json.dumps(body)) 155 | if not self.r.exists(self.expire): 156 | resp = self.session.put(self.endpoint, json=body) 157 | logging.info(resp.json()) 158 | self.r.set(self.expire, 1, ex=120) 159 | 160 | def clear_fw(self): 161 | logging.info("Clearing firewall rules") 162 | self.session.put(self.endpoint, json=[{"ip": "192.168.3.1"}]) 163 | logging.info("Clearing cache from redis") 164 | self.r.delete(self.key) 165 | 166 | 167 | if __name__ == "__main__": 168 | cf = Cloudflare() 169 | cf.ban_new_ip("192.168.1.1") 170 | -------------------------------------------------------------------------------- /yyetsweb/databases/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - __init__.py 5 | # 2023-03-17 18:57 6 | 7 | import logging 8 | import os 9 | import pathlib 10 | import sys 11 | 12 | import fakeredis 13 | import pymongo 14 | import redis 15 | 16 | DOUBAN_SEARCH = "https://www.douban.com/search?cat=1002&q={}" 17 | DOUBAN_DETAIL = "https://movie.douban.com/subject/{}/" 18 | 19 | lib_path = pathlib.Path(__file__).parent.parent.parent.joinpath("yyetsbot").resolve().as_posix() 20 | 21 | sys.path.append(lib_path) 22 | from fansub import BD2020, XL720, NewzmzOnline, ZhuixinfanOnline, ZimuxiaOnline 23 | 24 | logging.info( 25 | "Initialized: loading fansub...%s", 26 | (BD2020, XL720, NewzmzOnline, ZhuixinfanOnline, ZimuxiaOnline), 27 | ) 28 | 29 | mongo_client = pymongo.MongoClient( 30 | host=os.getenv("MONGO", "localhost"), 31 | connect=True, 32 | connectTimeoutMS=5000, 33 | serverSelectionTimeoutMS=5000, 34 | maxPoolSize=300, 35 | minPoolSize=50, 36 | maxIdleTimeMS=600000, 37 | ) 38 | db = mongo_client["zimuzu"] 39 | 40 | try: 41 | redis_client = redis.StrictRedis( 42 | host=os.getenv("REDIS", "localhost"), 43 | decode_responses=True, 44 | max_connections=100, 45 | ) 46 | redis_client.ping() 47 | except redis.exceptions.ConnectionError: 48 | logging.warning("%s Using fakeredis now... %s", "#" * 10, "#" * 10) 49 | redis_client = fakeredis.FakeStrictRedis() 50 | -------------------------------------------------------------------------------- /yyetsweb/databases/douban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import contextlib 4 | import logging 5 | import re 6 | from http import HTTPStatus 7 | from urllib.parse import unquote 8 | 9 | import requests 10 | from bs4 import BeautifulSoup 11 | from retry import retry 12 | 13 | from databases import DOUBAN_DETAIL, DOUBAN_SEARCH 14 | from databases.base import Mongo 15 | from databases.other import Captcha 16 | 17 | 18 | class Douban(Mongo): 19 | def get_douban_data(self, rid: int) -> dict: 20 | with contextlib.suppress(Exception): 21 | return self.find_douban(rid) 22 | return {"posterData": None} 23 | 24 | def get_douban_image(self, rid: int) -> bytes: 25 | db_data = self.get_douban_data(rid) 26 | return db_data["posterData"] 27 | 28 | @retry(IndexError, tries=3, delay=5) 29 | def find_douban(self, resource_id: int): 30 | session = requests.Session() 31 | ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 32 | session.headers.update({"User-Agent": ua}) 33 | 34 | douban_col = self.db["douban"] 35 | yyets_col = self.db["yyets"] 36 | data = douban_col.find_one({"resourceId": resource_id}, {"_id": False, "raw": False}) 37 | if data: 38 | logging.info("Existing data for %s", resource_id) 39 | return data 40 | 41 | # data not found, craw from douban 42 | projection = { 43 | "data.info.cnname": True, 44 | "data.info.enname": True, 45 | "data.info.aliasname": True, 46 | } 47 | names = yyets_col.find_one({"data.info.id": resource_id}, projection=projection) 48 | if names is None: 49 | return {} 50 | cname = names["data"]["info"]["cnname"] 51 | logging.info("cnname for douban is %s", cname) 52 | 53 | search_html = session.get(DOUBAN_SEARCH.format(cname)).text 54 | logging.info("Analysis search html...length %s", len(search_html)) 55 | soup = BeautifulSoup(search_html, "html.parser") 56 | douban_item = soup.find_all("div", class_="content") 57 | 58 | fwd_link = unquote(douban_item[0].a["href"]) 59 | douban_id = re.findall(r"https://movie\.douban\.com/subject/(\d*)/.*", fwd_link)[0] 60 | final_data = self.get_craw_data(cname, douban_id, resource_id, search_html, session) 61 | douban_col.insert_one(final_data.copy()) 62 | final_data.pop("raw") 63 | return final_data 64 | 65 | @staticmethod 66 | def get_craw_data(cname, douban_id, resource_id, search_html, session): 67 | detail_link = DOUBAN_DETAIL.format(douban_id) 68 | detail_html = session.get(detail_link).text 69 | logging.info("Analysis detail html...%s", detail_link) 70 | soup = BeautifulSoup(detail_html, "html.parser") 71 | 72 | directors = [i.text for i in (soup.find_all("a", rel="v:directedBy"))] 73 | release_date = poster_image_link = rating = year_text = intro = writers = episode_count = episode_duration = "" 74 | with contextlib.suppress(IndexError): 75 | episode_duration = soup.find_all("span", property="v:runtime")[0].text 76 | for i in soup.find_all("span", class_="pl"): 77 | if i.text == "编剧": 78 | writers = re.sub(r"\s", "", list(i.next_siblings)[1].text).split("/") 79 | if i.text == "集数:": 80 | episode_count = str(i.nextSibling) 81 | if i.text == "单集片长:" and not episode_duration: 82 | episode_duration = str(i.nextSibling) 83 | actors = [i.text for i in soup.find_all("a", rel="v:starring")] 84 | genre = [i.text for i in soup.find_all("span", property="v:genre")] 85 | 86 | with contextlib.suppress(IndexError): 87 | release_date = soup.find_all("span", property="v:initialReleaseDate")[0].text 88 | with contextlib.suppress(IndexError): 89 | poster_image_link = soup.find_all("div", id="mainpic")[0].a.img["src"] 90 | with contextlib.suppress(IndexError): 91 | rating = soup.find_all("strong", class_="ll rating_num")[0].text 92 | with contextlib.suppress(IndexError): 93 | year_text = re.sub(r"[()]", "", soup.find_all("span", class_="year")[0].text) 94 | with contextlib.suppress(IndexError): 95 | intro = re.sub(r"\s", "", soup.find_all("span", property="v:summary")[0].text) 96 | 97 | final_data = { 98 | "name": cname, 99 | "raw": { 100 | "search_url": DOUBAN_SEARCH.format(cname), 101 | "detail_url": detail_link, 102 | "search_html": search_html, 103 | "detail_html": detail_html, 104 | }, 105 | "doubanId": int(douban_id), 106 | "doubanLink": detail_link, 107 | "posterLink": poster_image_link, 108 | "posterData": session.get(poster_image_link).content, 109 | "resourceId": resource_id, 110 | "rating": rating, 111 | "actors": actors, 112 | "directors": directors, 113 | "genre": genre, 114 | "releaseDate": release_date, 115 | "episodeCount": episode_count, 116 | "episodeDuration": episode_duration, 117 | "writers": writers, 118 | "year": year_text, 119 | "introduction": intro, 120 | } 121 | return final_data 122 | 123 | 124 | class DoubanReport(Mongo): 125 | def get_error(self) -> dict: 126 | return dict(data=list(self.db["douban_error"].find(projection={"_id": False}))) 127 | 128 | def report_error(self, captcha: str, captcha_id: int, content: str, resource_id: int) -> dict: 129 | returned = {"status_code": 0, "message": ""} 130 | verify_result = Captcha().verify_code(captcha, captcha_id) 131 | if not verify_result["status"]: 132 | returned["status_code"] = HTTPStatus.BAD_REQUEST 133 | returned["message"] = verify_result["message"] 134 | return returned 135 | 136 | count = ( 137 | self.db["douban_error"] 138 | .update_one( 139 | {"resource_id": resource_id}, 140 | {"$push": {"content": content}}, 141 | upsert=True, 142 | ) 143 | .matched_count 144 | ) 145 | return dict(count=count) 146 | -------------------------------------------------------------------------------- /yyetsweb/databases/grafana.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import time 4 | from datetime import date, timedelta 5 | 6 | import pymongo 7 | 8 | from databases.base import Mongo 9 | 10 | 11 | class GrafanaQuery(Mongo): 12 | def get_grafana_data(self, date_series) -> str: 13 | condition = {"date": {"$in": date_series}} 14 | projection = {"_id": False} 15 | return self.db["metrics"].find(condition, projection) 16 | 17 | 18 | class Metrics(Mongo): 19 | def set_metrics(self, metrics_type: str, data: str): 20 | today = time.strftime("%Y-%m-%d", time.localtime()) 21 | if metrics_type == "viewSubtitle": 22 | self.db["subtitle"].find_one_and_update({"id": data}, {"$inc": {"views": 1}}) 23 | else: 24 | self.db["metrics"].update_one({"date": today}, {"$inc": {metrics_type: 1}}, upsert=True) 25 | 26 | def get_metrics(self, from_date: str, to_date: str) -> dict: 27 | start_int = [int(i) for i in from_date.split("-")] 28 | end_int = [int(i) for i in to_date.split("-")] 29 | sdate = date(*start_int) # start date 30 | edate = date(*end_int) # end date 31 | date_range = [str(sdate + timedelta(days=x)) for x in range((edate - sdate).days + 1)] 32 | condition = {"date": {"$in": date_range}} 33 | result = self.db["metrics"].find(condition, {"_id": False}).sort("date", pymongo.DESCENDING) 34 | 35 | return dict(metrics=list(result)) 36 | -------------------------------------------------------------------------------- /yyetsweb/databases/oauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | from hashlib import sha256 4 | 5 | from common.utils import ts_date 6 | from databases.base import Mongo 7 | 8 | 9 | class OAuthRegister(Mongo): 10 | def add_user(self, username, ip, browser, uid, source: "str"): 11 | uid = str(uid) 12 | # username = "Benny" 13 | user = self.db["users"].find_one({"uid": uid, "source": source}) 14 | if user and user.get("password"): 15 | # 直接注册的用户 16 | return {"status": "fail", "message": "第三方登录失败,用户名已存在"} 17 | elif user: 18 | # 已存在的oauth用户 19 | return {"status": "success", "message": "欢迎回来,即将跳转首页", "username": username} 20 | else: 21 | # 第一次oauth登录,假定一定会成功 22 | # TODO GitHub可以改用户名的,但是uid不会变,也许需要加unique index 23 | self.db["users"].insert_one( 24 | { 25 | "username": username, 26 | "date": ts_date(), 27 | "ip": ip, 28 | "browser": browser, 29 | "oldUser": True, 30 | "source": source, 31 | "uid": uid, 32 | "hash": sha256(username.encode("u8")).hexdigest(), 33 | } 34 | ) 35 | return { 36 | "status": "success", 37 | "message": "第三方登录成功,即将跳转首页", 38 | "username": username, 39 | } 40 | 41 | 42 | class GitHubOAuth2Login(OAuthRegister): 43 | pass 44 | 45 | 46 | class MSOAuth2Login(OAuthRegister): 47 | pass 48 | 49 | 50 | class GoogleOAuth2Login(OAuthRegister): 51 | pass 52 | 53 | 54 | class TwitterOAuth2Login(OAuthRegister): 55 | pass 56 | 57 | 58 | class FacebookAuth2Login(OAuthRegister): 59 | pass 60 | -------------------------------------------------------------------------------- /yyetsweb/databases/other.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import base64 4 | import json 5 | import logging 6 | import os 7 | import random 8 | import re 9 | import string 10 | import time 11 | from hashlib import sha256 12 | 13 | import pymongo 14 | import requests 15 | from bson import ObjectId 16 | from captcha.image import ImageCaptcha 17 | 18 | from common.utils import Cloudflare, ts_date 19 | from databases.base import Mongo, Redis 20 | 21 | cf = Cloudflare() 22 | 23 | captcha_ex = 60 * 10 24 | predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits) 25 | 26 | 27 | class Announcement(Mongo): 28 | def get_announcement(self, page: int, size: int) -> dict: 29 | condition = {} 30 | count = self.db["announcement"].count_documents(condition) 31 | data = ( 32 | self.db["announcement"] 33 | .find(condition, projection={"_id": True, "ip": False}) 34 | .sort("_id", pymongo.DESCENDING) 35 | .limit(size) 36 | .skip((page - 1) * size) 37 | ) 38 | data = list(data) 39 | for i in data: 40 | i["id"] = str(i["_id"]) 41 | i.pop("_id") 42 | return { 43 | "data": data, 44 | "count": count, 45 | } 46 | 47 | def add_announcement(self, username, content, ip, browser): 48 | construct = { 49 | "username": username, 50 | "ip": ip, 51 | "date": ts_date(), 52 | "browser": browser, 53 | "content": content, 54 | } 55 | self.db["announcement"].insert_one(construct) 56 | 57 | 58 | class Blacklist(Redis): 59 | def get_black_list(self): 60 | keys = self.r.keys("*") 61 | result = {} 62 | 63 | for key in keys: 64 | count = self.r.get(key) 65 | ttl = self.r.ttl(key) 66 | if ttl != -1: 67 | result[key] = dict(count=count, ttl=ttl) 68 | return result 69 | 70 | 71 | class Category(Mongo): 72 | def get_category(self, query: dict): 73 | page, size, douban = query["page"], query["size"], query["douban"] 74 | query.pop("page") 75 | query.pop("size") 76 | query.pop("douban") 77 | query_dict = {} 78 | for key, value in query.items(): 79 | query_dict[f"data.info.{key}"] = value 80 | logging.info("Query dict %s", query_dict) 81 | projection = {"_id": False, "data.list": False} 82 | data = self.db["yyets"].find(query_dict, projection=projection).limit(size).skip((page - 1) * size) 83 | count = self.db["yyets"].count_documents(query_dict) 84 | f = [] 85 | for item in data: 86 | if douban: 87 | douban_data = self.db["douban"].find_one( 88 | {"resourceId": item["data"]["info"]["id"]}, projection=projection 89 | ) 90 | if douban_data: 91 | douban_data["posterData"] = base64.b64encode(douban_data["posterData"]).decode("u8") 92 | item["data"]["info"]["douban"] = douban_data 93 | else: 94 | item["data"]["info"]["douban"] = {} 95 | f.append(item["data"]["info"]) 96 | return dict(data=f, count=count) 97 | 98 | 99 | class SpamProcess(Mongo): 100 | def ban_spam(self, obj_id: "str"): 101 | obj_id = ObjectId(obj_id) 102 | logging.info("Deleting spam %s", obj_id) 103 | spam = self.db["spam"].find_one({"_id": obj_id}) 104 | username = spam["username"] 105 | self.db["spam"].delete_many({"username": username}) 106 | # self.db["comment"].delete_many({"username": username}) 107 | cf.ban_new_ip(spam["ip"]) 108 | return {"status": True} 109 | 110 | def restore_spam(self, obj_id: "str"): 111 | obj_id = ObjectId(obj_id) 112 | spam = self.db["spam"].find_one({"_id": obj_id}, projection={"_id": False}) 113 | logging.info("Restoring spam %s", spam) 114 | self.db["comment"].insert_one(spam) 115 | self.db["spam"].delete_one({"_id": obj_id}) 116 | return {"status": True} 117 | 118 | @staticmethod 119 | def request_approval(document: "dict"): 120 | token = os.getenv("TOKEN") 121 | owner = os.getenv("OWNER") 122 | obj_id = document["_id"] 123 | data = { 124 | "text": json.dumps(document, ensure_ascii=False, indent=4), 125 | "chat_id": owner, 126 | "reply_markup": { 127 | "inline_keyboard": [ 128 | [ 129 | {"text": "approve", "callback_data": f"approve{obj_id}"}, 130 | {"text": "ban", "callback_data": f"ban{obj_id}"}, 131 | ] 132 | ] 133 | }, 134 | } 135 | api = f"https://api.telegram.org/bot{token}/sendMessage" 136 | resp = requests.post(api, json=data).json() 137 | logging.info("Telegram response: %s", resp) 138 | 139 | 140 | class Other(Mongo, Redis): 141 | def reset_top(self): 142 | # before resetting, save top data to history 143 | json_data = requests.get("http://127.0.0.1:8888/api/top").json() 144 | last_month = time.strftime("%Y-%m", time.localtime(time.time() - 3600 * 24)) 145 | json_data["date"] = last_month 146 | json_data["type"] = "top" 147 | self.db["history"].insert_one(json_data) 148 | # save all the views data to history 149 | projection = {"_id": False, "data.info.views": True, "data.info.id": True} 150 | data = self.db["yyets"].find({}, projection).sort("data.info.views", pymongo.DESCENDING) 151 | result = {"date": last_month, "type": "detail"} 152 | for datum in data: 153 | rid = str(datum["data"]["info"]["id"]) 154 | views = datum["data"]["info"]["views"] 155 | result[rid] = views 156 | self.db["history"].insert_one(result) 157 | # reset 158 | self.db["yyets"].update_many({}, {"$set": {"data.info.views": 0}}) 159 | 160 | def import_ban_user(self): 161 | usernames = self.db["users"].find({"status.disable": True}, projection={"username": True}) 162 | self.r.delete("user_blacklist") 163 | logging.info("Importing ban users to redis...%s", usernames) 164 | for username in [u["username"] for u in usernames]: 165 | self.r.hset("user_blacklist", username, 100) 166 | 167 | def fill_user_hash(self): 168 | users = self.db["users"].find({"hash": {"$exists": False}}, projection={"username": True}) 169 | # do it old school 170 | for user in users: 171 | logging.info("Filling hash for %s", user) 172 | username = user["username"] 173 | hash_value = sha256(username.encode("u8")).hexdigest() 174 | self.db["users"].update_one({"username": username}, {"$set": {"hash": hash_value}}) 175 | 176 | 177 | class Captcha(Redis): 178 | def get_captcha(self, captcha_id): 179 | chars = "".join([random.choice(predefined_str) for _ in range(4)]) 180 | image = ImageCaptcha() 181 | data = image.generate(chars) 182 | self.r.set(captcha_id, chars, ex=captcha_ex) 183 | return f"data:image/png;base64,{base64.b64encode(data.getvalue()).decode('ascii')}" 184 | 185 | def verify_code(self, user_input, captcha_id) -> dict: 186 | correct_code = self.r.get(captcha_id) 187 | if not correct_code: 188 | return {"status": False, "message": "验证码已过期"} 189 | if user_input.lower() == correct_code.lower(): 190 | self.r.delete(correct_code) 191 | return {"status": True, "message": "验证通过"} 192 | else: 193 | return {"status": False, "message": "验证码错误"} 194 | -------------------------------------------------------------------------------- /yyetsweb/go.mod: -------------------------------------------------------------------------------- 1 | module yyetsweb 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/glebarez/go-sqlite v1.19.1 8 | github.com/sirupsen/logrus v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/sonic v1.9.1 // indirect 13 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.14.0 // indirect 19 | github.com/goccy/go-json v0.10.2 // indirect 20 | github.com/google/uuid v1.3.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/kr/pretty v0.3.0 // indirect 24 | github.com/leodido/go-urn v1.2.4 // indirect 25 | github.com/mattn/go-isatty v0.0.19 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 29 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect 30 | github.com/rogpeppe/go-internal v1.8.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.11 // indirect 33 | golang.org/x/arch v0.3.0 // indirect 34 | golang.org/x/crypto v0.36.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/sys v0.31.0 // indirect 37 | golang.org/x/text v0.23.0 // indirect 38 | google.golang.org/protobuf v1.33.0 // indirect 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | modernc.org/libc v1.19.0 // indirect 42 | modernc.org/mathutil v1.5.0 // indirect 43 | modernc.org/memory v1.4.0 // indirect 44 | modernc.org/sqlite v1.19.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /yyetsweb/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import json 4 | 5 | from tornado import escape 6 | 7 | from common.utils import Cloudflare, setup_logger 8 | 9 | setup_logger() 10 | cf = Cloudflare() 11 | escape.json_encode = lambda value: json.dumps(value, ensure_ascii=False) 12 | -------------------------------------------------------------------------------- /yyetsweb/handlers/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import contextlib 4 | import importlib 5 | import json 6 | import logging 7 | import pathlib 8 | from concurrent.futures import ThreadPoolExecutor 9 | from http import HTTPStatus 10 | from pathlib import Path 11 | 12 | from tornado import gen, web 13 | from tornado.concurrent import run_on_executor 14 | 15 | from databases.base import redis_client 16 | from handlers import cf 17 | 18 | index = pathlib.Path(__file__).parent.parent.joinpath("templates", "index.html").as_posix() 19 | filename = Path(__file__).name.split(".")[0] 20 | 21 | 22 | class BaseHandler(web.RequestHandler): 23 | key = "user_blacklist" 24 | filename = filename 25 | 26 | executor = ThreadPoolExecutor(200) 27 | 28 | def __init__(self, application, request, **kwargs): 29 | super().__init__(application, request, **kwargs) 30 | self.json = {} 31 | with contextlib.suppress(ValueError): 32 | self.json: dict = json.loads(self.request.body) 33 | class_name = self.__class__.__name__.split("Handler")[0] 34 | module = importlib.import_module(f"databases.{self.filename}") 35 | 36 | self.instance = getattr(module, class_name, lambda: 1)() 37 | self.r = redis_client 38 | 39 | def add_tauri(self): 40 | origin = self.request.headers.get("origin", "") 41 | allow_origins = ["tauri://localhost", "https://tauri.localhost"] 42 | if origin in allow_origins: 43 | self.set_header("Access-Control-Allow-Origin", origin) 44 | 45 | def prepare(self): 46 | self.add_tauri() 47 | if self.check_request(): 48 | self.set_status(HTTPStatus.FORBIDDEN) 49 | self.finish() 50 | 51 | def data_received(self, chunk): 52 | pass 53 | 54 | def check_request(self): 55 | ban = self.__ip_check() 56 | user = self.__user_check() 57 | result = ban or user 58 | if result: 59 | self.ban() 60 | return result 61 | 62 | def get_real_ip(self): 63 | x_real = self.request.headers.get("X-Real-IP") 64 | remote_ip = self.request.remote_ip 65 | logging.debug("X-Real-IP:%s, Remote-IP:%s", x_real, remote_ip) 66 | return x_real or remote_ip 67 | 68 | def ban(self): 69 | ip = self.get_real_ip() 70 | self.r.incr(ip) 71 | count = int(self.r.get(ip)) 72 | # ban rule: (count-10)*600 73 | if count <= 10: 74 | ex = 120 75 | else: 76 | ex = (count - 10) * 600 77 | if count >= 30: 78 | cf.ban_new_ip(ip) 79 | self.r.set(ip, count, ex) 80 | user = self.get_current_user() 81 | if user: 82 | self.r.hincrby(self.key, user) 83 | 84 | def get_current_user(self) -> str: 85 | username = self.get_secure_cookie("username") or b"" 86 | return username.decode("u8") 87 | 88 | def __user_check(self): 89 | count = self.r.hget(self.key, self.get_current_user()) or 0 90 | count = int(count) 91 | if count >= 20: 92 | return True 93 | 94 | def __ip_check(self): 95 | d = self.r.get(self.get_real_ip()) or 0 96 | if int(d) >= 10: 97 | return True 98 | 99 | def write_error(self, status_code, **kwargs): 100 | if status_code in [ 101 | HTTPStatus.FORBIDDEN, 102 | HTTPStatus.UNAUTHORIZED, 103 | HTTPStatus.NOT_FOUND, 104 | HTTPStatus.INTERNAL_SERVER_ERROR, 105 | ]: 106 | self.write(str(kwargs.get("exc_info"))) 107 | 108 | 109 | class IndexHandler(BaseHandler): 110 | @run_on_executor() 111 | def send_index(self): 112 | with open(index, encoding="u8") as f: 113 | html = f.read() 114 | return html 115 | 116 | @gen.coroutine 117 | def get(self): 118 | resp = yield self.send_index() 119 | self.write(resp) 120 | 121 | 122 | class NotFoundHandler(BaseHandler): 123 | def get(self): 124 | # for react app 125 | self.render(index) 126 | -------------------------------------------------------------------------------- /yyetsweb/handlers/comment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | from http import HTTPStatus 4 | from pathlib import Path 5 | 6 | from tornado import gen, web 7 | from tornado.concurrent import run_on_executor 8 | 9 | from common.utils import hide_phone 10 | from handlers.base import BaseHandler 11 | 12 | filename = Path(__file__).name.split(".")[0] 13 | 14 | 15 | class CommentHandler(BaseHandler): 16 | filename = filename 17 | 18 | @run_on_executor() 19 | def get_comment(self): 20 | query_id = self.get_argument("resource_id", "0") 21 | resource_id = int(query_id) if query_id.isdigit() else 0 22 | 23 | size = int(self.get_argument("size", "5")) 24 | page = int(self.get_argument("page", "1")) 25 | inner_size = int(self.get_argument("inner_size", "3")) 26 | inner_page = int(self.get_argument("inner_page", "1")) 27 | comment_id = self.get_argument("comment_id", None) 28 | sort = self.get_argument("sort", "newest") 29 | 30 | if not resource_id: 31 | self.set_status(HTTPStatus.BAD_REQUEST) 32 | return {"status": False, "message": "请提供resource id"} 33 | comment_data = self.instance.get_comment( 34 | resource_id, 35 | page, 36 | size, 37 | sort=sort, 38 | inner_size=inner_size, 39 | inner_page=inner_page, 40 | comment_id=comment_id, 41 | ) 42 | hide_phone((comment_data["data"])) 43 | return comment_data 44 | 45 | @run_on_executor() 46 | def add_comment(self): 47 | payload = self.json 48 | captcha = payload["captcha"] 49 | captcha_id = payload["id"] 50 | content = payload["content"] 51 | resource_id = payload["resource_id"] 52 | comment_id = payload.get("comment_id") 53 | 54 | real_ip = self.get_real_ip() 55 | username = self.get_current_user() 56 | browser = self.request.headers["user-agent"] 57 | 58 | result = self.instance.add_comment( 59 | captcha, 60 | captcha_id, 61 | content, 62 | resource_id, 63 | real_ip, 64 | username, 65 | browser, 66 | comment_id, 67 | ) 68 | self.set_status(result["status_code"]) 69 | return result 70 | 71 | @run_on_executor() 72 | def delete_comment(self): 73 | # need resource_id & id 74 | # payload = {"id": "obj_id"} 75 | payload = self.json 76 | username = self.get_current_user() 77 | comment_id = payload["comment_id"] 78 | 79 | if self.instance.is_admin(username): 80 | result = self.instance.delete_comment(comment_id) 81 | self.set_status(result["status_code"]) 82 | return result 83 | else: 84 | self.set_status(HTTPStatus.UNAUTHORIZED) 85 | return {"count": 0, "message": "You're unauthorized to delete comment."} 86 | 87 | @gen.coroutine 88 | def get(self): 89 | resp = yield self.get_comment() 90 | self.write(resp) 91 | 92 | @gen.coroutine 93 | @web.authenticated 94 | def post(self): 95 | resp = yield self.add_comment() 96 | self.write(resp) 97 | 98 | @gen.coroutine 99 | @web.authenticated 100 | def delete(self): 101 | resp = yield self.delete_comment() 102 | self.write(resp) 103 | 104 | 105 | class CommentReactionHandler(BaseHandler): 106 | filename = filename 107 | 108 | @run_on_executor() 109 | def comment_reaction(self): 110 | self.json.update(method=self.request.method) 111 | username = self.get_current_user() 112 | result = self.instance.react_comment(username, self.json) 113 | self.set_status(result.get("status_code")) 114 | return result 115 | 116 | @gen.coroutine 117 | @web.authenticated 118 | def post(self): 119 | resp = yield self.comment_reaction() 120 | self.write(resp) 121 | 122 | @gen.coroutine 123 | @web.authenticated 124 | def delete(self): 125 | resp = yield self.comment_reaction() 126 | self.write(resp) 127 | 128 | 129 | class CommentChildHandler(CommentHandler): 130 | filename = filename 131 | 132 | @run_on_executor() 133 | def get_comment(self): 134 | parent_id = self.get_argument("parent_id", "0") 135 | size = int(self.get_argument("size", "3")) 136 | page = int(self.get_argument("page", "1")) 137 | 138 | if not parent_id: 139 | self.set_status(HTTPStatus.BAD_REQUEST) 140 | return {"status": False, "message": "请提供 parent_id"} 141 | comment_data = self.instance.get_comment(parent_id, page, size) 142 | hide_phone((comment_data["data"])) 143 | return comment_data 144 | 145 | @gen.coroutine 146 | def get(self): 147 | resp = yield self.get_comment() 148 | self.write(resp) 149 | 150 | 151 | class CommentNewestHandler(CommentHandler): 152 | filename = filename 153 | 154 | @run_on_executor() 155 | def get_comment(self): 156 | size = int(self.get_argument("size", "5")) 157 | page = int(self.get_argument("page", "1")) 158 | 159 | comment_data = self.instance.get_comment(page, size) 160 | hide_phone((comment_data["data"])) 161 | return comment_data 162 | 163 | @gen.coroutine 164 | def get(self): 165 | resp = yield self.get_comment() 166 | self.write(resp) 167 | 168 | 169 | class CommentSearchHandler(CommentHandler): 170 | filename = filename 171 | 172 | @run_on_executor() 173 | def search_comment(self): 174 | size = int(self.get_argument("size", "5")) 175 | page = int(self.get_argument("page", "1")) 176 | keyword = self.get_argument("keyword", "") 177 | comment_data = self.instance.get_comment(page, size, keyword) 178 | hide_phone((comment_data["data"])) 179 | return comment_data 180 | 181 | @gen.coroutine 182 | def get(self): 183 | resp = yield self.search_comment() 184 | self.write(resp) 185 | 186 | 187 | class NotificationHandler(BaseHandler): 188 | filename = filename 189 | 190 | @run_on_executor() 191 | def get_notification(self): 192 | username = self.get_current_user() 193 | size = int(self.get_argument("size", "5")) 194 | page = int(self.get_argument("page", "1")) 195 | 196 | return self.instance.get_notification(username, page, size) 197 | 198 | @run_on_executor() 199 | def update_notification(self): 200 | username = self.get_current_user() 201 | verb = self.json["verb"] 202 | comment_id = self.json["comment_id"] 203 | if verb not in ["read", "unread"]: 204 | self.set_status(HTTPStatus.BAD_REQUEST) 205 | return {"status": False, "message": "verb: read or unread"} 206 | self.set_status(HTTPStatus.CREATED) 207 | return self.instance.update_notification(username, verb, comment_id) 208 | 209 | @gen.coroutine 210 | @web.authenticated 211 | def get(self): 212 | resp = yield self.get_notification() 213 | self.write(resp) 214 | 215 | @gen.coroutine 216 | @web.authenticated 217 | def patch(self): 218 | resp = yield self.update_notification() 219 | self.write(resp) 220 | -------------------------------------------------------------------------------- /yyetsweb/handlers/douban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | from http import HTTPStatus 4 | from pathlib import Path 5 | 6 | import filetype 7 | from tornado import gen 8 | from tornado.concurrent import run_on_executor 9 | 10 | from handlers.base import BaseHandler 11 | 12 | filename = Path(__file__).name.split(".")[0] 13 | 14 | 15 | class DoubanHandler(BaseHandler): 16 | filename = filename 17 | 18 | @run_on_executor() 19 | def douban_data(self): 20 | rid = self.get_query_argument("resource_id") 21 | data = self.instance.get_douban_data(int(rid)) 22 | data.pop("posterData", None) 23 | if not data: 24 | self.set_status(HTTPStatus.NOT_FOUND) 25 | return data 26 | 27 | def get_image(self) -> bytes: 28 | rid = self.get_query_argument("resource_id") 29 | return self.instance.get_douban_image(int(rid)) 30 | 31 | @gen.coroutine 32 | def get(self): 33 | _type = self.get_query_argument("type", None) 34 | if _type == "image": 35 | data = self.get_image() 36 | self.set_header("content-type", filetype.guess_mime(data)) 37 | self.write(data) 38 | else: 39 | resp = yield self.douban_data() 40 | self.write(resp) 41 | 42 | 43 | class DoubanReportHandler(BaseHandler): 44 | class_name = "DoubanReportResource" 45 | 46 | @run_on_executor() 47 | def get_error(self): 48 | return self.instance.get_error() 49 | 50 | @run_on_executor() 51 | def report_error(self): 52 | data = self.json 53 | user_captcha = data["captcha_id"] 54 | captcha_id = data["id"] 55 | content = data["content"] 56 | resource_id = data["resource_id"] 57 | returned = self.instance.report_error(user_captcha, captcha_id, content, resource_id) 58 | status_code = returned.get("status_code", HTTPStatus.CREATED) 59 | self.set_status(status_code) 60 | return self.instance.report_error(user_captcha, captcha_id, content, resource_id) 61 | 62 | @gen.coroutine 63 | def post(self): 64 | resp = yield self.report_error() 65 | self.write(resp) 66 | 67 | @gen.coroutine 68 | def get(self): 69 | resp = yield self.get_error() 70 | self.write(resp) 71 | -------------------------------------------------------------------------------- /yyetsweb/handlers/grafana.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import json 4 | import time 5 | from datetime import date, timedelta 6 | from http import HTTPStatus 7 | from pathlib import Path 8 | 9 | from tornado import gen 10 | from tornado.concurrent import run_on_executor 11 | 12 | from handlers.base import BaseHandler 13 | 14 | filename = Path(__file__).name.split(".")[0] 15 | 16 | 17 | class MetricsHandler(BaseHandler): 18 | filename = filename 19 | 20 | @run_on_executor() 21 | def set_metrics(self): 22 | payload = self.json 23 | metrics_type = payload.get("type", self.get_query_argument("type", "unknown")) 24 | _id = payload.get("id") 25 | 26 | self.instance.set_metrics(metrics_type, _id) 27 | self.set_status(HTTPStatus.CREATED) 28 | return {} 29 | 30 | @run_on_executor() 31 | def get_metrics(self): 32 | if not self.instance.is_admin(self.get_current_user()): 33 | self.set_status(HTTPStatus.NOT_FOUND) 34 | return "" 35 | 36 | # only return latest 7 days. with days parameter to generate different range 37 | from_date = self.get_query_argument("from", None) 38 | to_date = self.get_query_argument("to", None) 39 | if to_date is None: 40 | to_date = time.strftime("%Y-%m-%d", time.localtime()) 41 | if from_date is None: 42 | from_date = time.strftime("%Y-%m-%d", time.localtime(time.time() - 3600 * 24 * 7)) 43 | 44 | return self.instance.get_metrics(from_date, to_date) 45 | 46 | @gen.coroutine 47 | def get(self): 48 | resp = yield self.get_metrics() 49 | self.write(resp) 50 | 51 | @gen.coroutine 52 | def post(self): 53 | resp = yield self.set_metrics() 54 | self.write(resp) 55 | 56 | @gen.coroutine 57 | def options(self): 58 | self.add_tauri() 59 | self.set_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") 60 | self.set_status(HTTPStatus.NO_CONTENT) 61 | self.finish() 62 | 63 | 64 | class GrafanaIndexHandler(BaseHandler): 65 | filename = filename 66 | 67 | def get(self): 68 | self.write({}) 69 | 70 | 71 | class GrafanaSearchHandler(BaseHandler): 72 | filename = filename 73 | 74 | def post(self): 75 | data = [ 76 | "resource", 77 | "top", 78 | "home", 79 | "search", 80 | "extra", 81 | "discuss", 82 | "multiDownload", 83 | "download", 84 | "user", 85 | "share", 86 | "me", 87 | "database", 88 | "help", 89 | "backOld", 90 | "favorite", 91 | "unFavorite", 92 | "comment", 93 | ] 94 | self.write(json.dumps(data)) 95 | 96 | 97 | class GrafanaQueryHandler(BaseHandler): 98 | filename = filename 99 | 100 | @staticmethod 101 | def generate_date_series(start: str, end: str) -> list: 102 | start_int = [int(i) for i in start.split("-")] 103 | end_int = [int(i) for i in end.split("-")] 104 | sdate = date(*start_int) # start date 105 | edate = date(*end_int) # end date 106 | 107 | delta = edate - sdate # as timedelta 108 | days = [] 109 | for i in range(delta.days + 1): 110 | day = sdate + timedelta(days=i) 111 | days.append(day.strftime("%Y-%m-%d")) 112 | return days 113 | 114 | @staticmethod 115 | def time_str_int(text): 116 | return time.mktime(time.strptime(text, "%Y-%m-%d")) 117 | 118 | def post(self): 119 | payload = self.json 120 | start = payload["range"]["from"].split("T")[0] 121 | end = payload["range"]["to"].split("T")[0] 122 | date_series = self.generate_date_series(start, end) 123 | targets = [i["target"] for i in payload["targets"] if i["target"]] 124 | grafana_data = [] 125 | for target in targets: 126 | data_points = [] 127 | result = self.instance.get_grafana_data(date_series) 128 | i: dict 129 | for i in result: 130 | datum = [i[target], self.time_str_int(i["date"]) * 1000] if i.get(target) else [] 131 | data_points.append(datum) 132 | temp = {"target": target, "datapoints": data_points} 133 | grafana_data.append(temp) 134 | self.write(json.dumps(grafana_data)) 135 | -------------------------------------------------------------------------------- /yyetsweb/handlers/oauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import logging 4 | import os 5 | from pathlib import Path 6 | from urllib.parse import urlencode 7 | 8 | import requests 9 | from tornado.auth import GoogleOAuth2Mixin, OAuth2Mixin, TwitterMixin 10 | 11 | from handlers.base import BaseHandler 12 | 13 | filename = Path(__file__).name.split(".")[0] 14 | 15 | 16 | class OAuth2Handler(BaseHandler, OAuth2Mixin): 17 | filename = filename 18 | _OAUTH_AUTHORIZE_URL = "" 19 | _OAUTH_ACCESS_TOKEN_URL = "" 20 | _OAUTH_API_REQUEST_URL = "" 21 | 22 | def add_oauth_user(self, username, unique, source): 23 | logging.info("User %s login with %s now...", username, source) 24 | ip = self.get_real_ip() 25 | browser = self.request.headers["user-agent"] 26 | result = self.instance.add_user(username, ip, browser, unique, source) 27 | if result["status"] == "success": 28 | self.set_secure_cookie("username", username, 365) 29 | self.redirect("/login?" + urlencode(result)) 30 | 31 | def get_authenticated_user(self, client_id: str, client_secret: str, code: str, extra_fields: dict = None): 32 | args = {"code": code, "client_id": client_id, "client_secret": client_secret} 33 | if extra_fields: 34 | args.update(extra_fields) 35 | return requests.post( 36 | self._OAUTH_ACCESS_TOKEN_URL, 37 | headers={"Accept": "application/json"}, 38 | data=args, 39 | ).json() 40 | 41 | def oauth2_sync_request(self, access_token, extra_fields=None): 42 | return requests.get( 43 | self._OAUTH_API_REQUEST_URL, 44 | headers={"Authorization": f"Bearer {access_token}"}, 45 | params=extra_fields, 46 | ).json() 47 | 48 | def get_secret(self, settings_key): 49 | settings = self.settings.get(settings_key) 50 | client_id = settings.get("key") 51 | client_secret = settings.get("secret") 52 | redirect_uri = os.getenv("DOMAIN") + self.request.path 53 | return client_id, client_secret, redirect_uri 54 | 55 | 56 | class GitHubOAuth2LoginHandler(OAuth2Handler): 57 | _OAUTH_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" 58 | _OAUTH_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" 59 | _OAUTH_API_REQUEST_URL = "https://api.github.com/user" 60 | 61 | def get(self): 62 | client_id, client_secret, redirect_uri = self.get_secret("github_oauth") 63 | code = self.get_argument("code", None) 64 | if code: 65 | access = self.get_authenticated_user(client_id, client_secret, code) 66 | resp = self.oauth2_sync_request(access["access_token"]) 67 | username = resp["login"] 68 | db_id = resp["id"] 69 | self.add_oauth_user(username, db_id, "GitHub") 70 | else: 71 | self.authorize_redirect( 72 | redirect_uri=redirect_uri, 73 | client_id=client_id, 74 | scope=[], 75 | response_type="code", 76 | ) 77 | 78 | 79 | class MSOAuth2LoginHandler(OAuth2Handler): 80 | _OAUTH_AUTHORIZE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" 81 | _OAUTH_ACCESS_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token" 82 | _OAUTH_API_REQUEST_URL = "https://graph.microsoft.com/v1.0/me" 83 | 84 | def get(self): 85 | client_id, client_secret, redirect_uri = self.get_secret("ms_oauth") 86 | code = self.get_argument("code", None) 87 | if code: 88 | access = self.get_authenticated_user( 89 | client_id, 90 | client_secret, 91 | code, 92 | {"grant_type": "authorization_code", "redirect_uri": redirect_uri}, 93 | ) 94 | resp = self.oauth2_sync_request(access["access_token"]) 95 | email = resp["userPrincipalName"] 96 | uid = resp["id"] 97 | self.add_oauth_user(email, uid, "Microsoft") 98 | 99 | else: 100 | self.authorize_redirect( 101 | redirect_uri=redirect_uri, 102 | client_id=client_id, 103 | scope=["https://graph.microsoft.com/User.Read"], 104 | response_type="code", 105 | ) 106 | 107 | 108 | class GoogleOAuth2LoginHandler(GoogleOAuth2Mixin, OAuth2Handler): 109 | async def get(self): 110 | redirect_uri = os.getenv("DOMAIN") + self.request.path 111 | code = self.get_argument("code", None) 112 | if code: 113 | access = await self.get_authenticated_user(redirect_uri=redirect_uri, code=code) 114 | user = await self.oauth2_request( 115 | "https://www.googleapis.com/oauth2/v1/userinfo", 116 | access_token=access["access_token"], 117 | ) 118 | email = user["email"] 119 | # Google's email can't be changed 120 | self.add_oauth_user(email, email, "Google") 121 | else: 122 | self.authorize_redirect( 123 | redirect_uri=redirect_uri, 124 | client_id=self.settings["google_oauth"]["key"], 125 | scope=["email"], 126 | response_type="code", 127 | extra_params={"approval_prompt": "auto"}, 128 | ) 129 | 130 | 131 | class TwitterOAuth2LoginHandler(TwitterMixin, OAuth2Handler): 132 | async def get(self): 133 | if self.get_argument("oauth_token", None): 134 | user = await self.get_authenticated_user() 135 | username = user["username"] 136 | id_str = user["id_str"] 137 | self.add_oauth_user(username, id_str, "Twitter") 138 | else: 139 | await self.authorize_redirect(extra_params={"x_auth_access_type": "read"}) 140 | 141 | 142 | class FacebookAuth2LoginHandler(OAuth2Handler): 143 | _OAUTH_AUTHORIZE_URL = "https://www.facebook.com/v16.0/dialog/oauth" 144 | _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token" 145 | _OAUTH_API_REQUEST_URL = "https://graph.facebook.com/me" 146 | 147 | def get(self): 148 | client_id, client_secret, redirect_uri = self.get_secret("fb_oauth") 149 | code = self.get_argument("code", None) 150 | if code: 151 | access = self.get_authenticated_user(client_id, client_secret, code, {"redirect_uri": redirect_uri}) 152 | resp = self.oauth2_sync_request(access["access_token"], {"fields": "name,id"}) 153 | # Facebook doesn't allow to get email except for business accounts 154 | uid = resp["id"] 155 | email = "{}_{}".format(resp["name"], uid) 156 | self.add_oauth_user(email, uid, "Facebook") 157 | 158 | else: 159 | self.authorize_redirect( 160 | redirect_uri=redirect_uri, 161 | client_id=client_id, 162 | ) 163 | -------------------------------------------------------------------------------- /yyetsweb/handlers/other.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import logging 4 | import os 5 | import pathlib 6 | import time 7 | from hashlib import sha1 8 | from http import HTTPStatus 9 | from pathlib import Path 10 | 11 | from tornado import gen, web 12 | from tornado.concurrent import run_on_executor 13 | 14 | from common.utils import ts_date 15 | from databases.base import Redis 16 | from handlers.base import BaseHandler 17 | 18 | filename = Path(__file__).name.split(".")[0] 19 | 20 | 21 | class AnnouncementHandler(BaseHandler): 22 | filename = filename 23 | 24 | @run_on_executor() 25 | def get_announcement(self): 26 | size = int(self.get_argument("size", "5")) 27 | page = int(self.get_argument("page", "1")) 28 | return self.instance.get_announcement(page, size) 29 | 30 | @run_on_executor() 31 | def add_announcement(self): 32 | username = self.get_current_user() 33 | if not self.instance.is_admin(username): 34 | self.set_status(HTTPStatus.FORBIDDEN) 35 | return {"message": "只有管理员可以设置公告"} 36 | 37 | payload = self.json 38 | content = payload["content"] 39 | real_ip = self.get_real_ip() 40 | browser = self.request.headers["user-agent"] 41 | 42 | self.instance.add_announcement(username, content, real_ip, browser) 43 | self.set_status(HTTPStatus.CREATED) 44 | return {"message": "添加成功"} 45 | 46 | @gen.coroutine 47 | def get(self): 48 | resp = yield self.get_announcement() 49 | self.write(resp) 50 | 51 | @gen.coroutine 52 | @web.authenticated 53 | def post(self): 54 | resp = yield self.add_announcement() 55 | self.write(resp) 56 | 57 | 58 | class DBDumpHandler(BaseHandler): 59 | @staticmethod 60 | def sizeof_fmt(num: int, suffix="B"): 61 | for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: 62 | if abs(num) < 1024.0: 63 | return "%3.1f%s%s" % (num, unit, suffix) 64 | num /= 1024.0 65 | return "%.1f%s%s" % (num, "Yi", suffix) 66 | 67 | @staticmethod 68 | def checksum(file_path) -> str: 69 | sha = sha1() 70 | try: 71 | with open(file_path, "rb") as f: 72 | sha.update(f.read()) 73 | checksum = sha.hexdigest() 74 | except Exception as e: 75 | checksum = str(e) 76 | 77 | return checksum 78 | 79 | @run_on_executor() 80 | @Redis.cache(3600) 81 | def get_hash(self): 82 | file_list = [ 83 | "templates/dump/yyets_mongo.gz", 84 | "templates/dump/yyets_mysql.zip", 85 | "templates/dump/yyets_sqlite.zip", 86 | ] 87 | result = {} 88 | for fp in file_list: 89 | checksum = self.checksum(fp) 90 | creation = ts_date(os.stat(fp).st_ctime) 91 | size = self.sizeof_fmt(os.stat(fp).st_size) 92 | result[Path(fp).name] = { 93 | "checksum": checksum, 94 | "date": creation, 95 | "size": size, 96 | } 97 | 98 | return result 99 | 100 | @gen.coroutine 101 | def get(self): 102 | resp = yield self.get_hash() 103 | self.write(resp) 104 | 105 | 106 | class CategoryHandler(BaseHandler): 107 | filename = filename 108 | 109 | @run_on_executor() 110 | def get_data(self): 111 | self.json = {k: self.get_argument(k) for k in self.request.arguments} 112 | self.json["size"] = int(self.json.get("size", 15)) 113 | self.json["page"] = int(self.json.get("page", 1)) 114 | self.json["douban"] = self.json.get("douban", False) 115 | return self.instance.get_category(self.json) 116 | 117 | @gen.coroutine 118 | def get(self): 119 | resp = yield self.get_data() 120 | self.write(resp) 121 | 122 | 123 | class CaptchaHandler(BaseHandler): 124 | filename = filename 125 | 126 | @run_on_executor() 127 | def verify_captcha(self): 128 | data = self.json 129 | captcha_id = data.get("id", None) 130 | userinput = data.get("captcha", None) 131 | if captcha_id is None or userinput is None: 132 | self.set_status(HTTPStatus.BAD_REQUEST) 133 | return "Please supply id or captcha parameter." 134 | returned = self.instance.verify_code(userinput, captcha_id) 135 | status_code = returned.get("status") 136 | if not status_code: 137 | self.set_status(HTTPStatus.FORBIDDEN) 138 | return returned 139 | 140 | @run_on_executor() 141 | def captcha(self): 142 | request_id = self.get_argument("id", None) 143 | if request_id is None: 144 | self.set_status(HTTPStatus.BAD_REQUEST) 145 | return "Please supply id parameter." 146 | 147 | return self.instance.get_captcha(request_id) 148 | 149 | @gen.coroutine 150 | def get(self): 151 | resp = yield self.captcha() 152 | self.write(resp) 153 | 154 | @gen.coroutine 155 | def post(self): 156 | resp = yield self.verify_captcha() 157 | self.write(resp) 158 | 159 | 160 | class BlacklistHandler(BaseHandler): 161 | filename = filename 162 | 163 | @run_on_executor() 164 | def get_black_list(self): 165 | return self.instance.get_black_list() 166 | 167 | @gen.coroutine 168 | def get(self): 169 | resp = yield self.get_black_list() 170 | self.write(resp) 171 | 172 | 173 | class SpamProcessHandler(BaseHandler): 174 | filename = filename 175 | 176 | def process(self, method): 177 | obj_id = self.json.get("obj_id") 178 | token = self.json.get("token") 179 | ua = self.request.headers["user-agent"] 180 | ip = self.get_real_ip() 181 | logging.info("Authentication %s(%s) for spam API now...", ua, ip) 182 | if token == os.getenv("TOKEN"): 183 | return getattr(self.instance, method)(obj_id) 184 | else: 185 | self.set_status(HTTPStatus.FORBIDDEN) 186 | return { 187 | "status": False, 188 | "message": "This token is not allowed to access this API", 189 | } 190 | 191 | @gen.coroutine 192 | def post(self): 193 | self.write(self.process("restore_spam")) 194 | 195 | @gen.coroutine 196 | def delete(self): 197 | self.write(self.process("ban_spam")) 198 | -------------------------------------------------------------------------------- /yyetsweb/handlers/resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import os 4 | from http import HTTPStatus 5 | from pathlib import Path 6 | 7 | from tornado import gen 8 | from tornado.concurrent import run_on_executor 9 | 10 | from handlers.base import BaseHandler 11 | 12 | filename = Path(__file__).name.split(".")[0] 13 | 14 | 15 | class SubtitleDownloadHandler(BaseHandler): 16 | filename = filename 17 | 18 | @run_on_executor() 19 | def find_and_download(self): 20 | file = self.json.get("file") 21 | _id = self.json.get("id") 22 | self.set_header("x-filename", Path(file).name) 23 | p = Path(__file__).parent.parent.joinpath("subtitle_data", file) 24 | self.set_header("Content-Type", "application/bin") 25 | try: 26 | data = p.read_bytes() 27 | self.instance.add_download(_id) 28 | return data 29 | except FileNotFoundError: 30 | self.set_status(HTTPStatus.NOT_FOUND) 31 | return b"" 32 | 33 | @gen.coroutine 34 | def post(self): 35 | resp = yield self.find_and_download() 36 | self.write(resp) 37 | 38 | 39 | class ResourceHandler(BaseHandler): 40 | filename = filename 41 | 42 | @run_on_executor() 43 | def get_resource_data(self): 44 | query = self.get_query_argument("id", None) 45 | resource_id = int(query) if query.isdigit() else 0 46 | username = self.get_current_user() 47 | data = self.instance.get_resource_data(resource_id, username) 48 | if not data: 49 | self.ban() 50 | self.set_status(HTTPStatus.NOT_FOUND) 51 | data = {} 52 | 53 | return data 54 | 55 | @run_on_executor() 56 | def search_resource(self): 57 | kw = self.get_query_argument("keyword").lower() 58 | search_type = self.get_query_argument("type", "default") 59 | self.set_header("search-engine", "Meilisearch" if os.getenv("MEILISEARCH") else "MongoDB") 60 | return self.instance.search_resource(kw, search_type) 61 | 62 | @gen.coroutine 63 | def get(self): 64 | if self.get_query_argument("id", None): 65 | resp = yield self.get_resource_data() 66 | elif self.get_query_argument("keyword", None): 67 | resp = yield self.search_resource() 68 | else: 69 | resp = "error" 70 | self.write(resp) 71 | 72 | 73 | class ResourceLatestHandler(BaseHandler): 74 | filename = filename 75 | 76 | @run_on_executor() 77 | def get_latest(self): 78 | size = int(self.get_query_argument("size", "100")) 79 | result = self.instance.get_latest_resource() 80 | result["data"] = result["data"][:size] 81 | return result 82 | 83 | @gen.coroutine 84 | def get(self): 85 | resp = yield self.get_latest() 86 | self.write(resp) 87 | 88 | 89 | class TopHandler(BaseHandler): 90 | filename = filename 91 | 92 | def get_user_like(self) -> list: 93 | username = self.get_current_user() 94 | return self.instance.get_user_like(username) 95 | 96 | def get_most(self) -> list: 97 | return self.instance.get_most() 98 | 99 | @run_on_executor() 100 | def get_top_resource(self): 101 | return self.instance.get_top_resource() 102 | 103 | @gen.coroutine 104 | def get(self): 105 | resp = yield self.get_top_resource() 106 | self.write(resp) 107 | 108 | 109 | class NameHandler(BaseHandler): 110 | filename = filename 111 | 112 | @run_on_executor() 113 | def get_names(self): 114 | is_readable = self.get_query_argument("human", None) 115 | return self.instance.get_names(is_readable) 116 | 117 | @gen.coroutine 118 | def get(self): 119 | resp = yield self.get_names() 120 | self.write(resp) 121 | 122 | 123 | class AdsenseStatusHandler(BaseHandler): 124 | @run_on_executor() 125 | def get_adsense_status(self): 126 | return {"data": os.getenv("HIDE_ADSENSE", "").split(",")} 127 | 128 | @gen.coroutine 129 | def get(self): 130 | resp = yield self.get_adsense_status() 131 | self.write(resp) 132 | -------------------------------------------------------------------------------- /yyetsweb/handlers/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | from http import HTTPStatus 4 | from pathlib import Path 5 | 6 | from tornado import gen, web 7 | from tornado.concurrent import run_on_executor 8 | 9 | from handlers.base import BaseHandler 10 | 11 | filename = Path(__file__).name.split(".")[0] 12 | 13 | 14 | class UserHandler(BaseHandler): 15 | filename = filename 16 | 17 | def set_login(self, username): 18 | self.set_secure_cookie("username", username, 365) 19 | 20 | @run_on_executor() 21 | def login(self): 22 | data = self.json 23 | username = data["username"] 24 | password = data["password"] 25 | captcha = data.get("captcha") 26 | captcha_id = data.get("captcha_id", "") 27 | ip = self.get_real_ip() 28 | browser = self.request.headers["user-agent"] 29 | 30 | response = self.instance.login_user(username, password, captcha, captcha_id, ip, browser) 31 | if response["status_code"] in (HTTPStatus.CREATED, HTTPStatus.OK): 32 | self.set_login(username) 33 | else: 34 | self.set_status(response["status_code"]) 35 | 36 | return response 37 | 38 | @run_on_executor() 39 | def update_info(self): 40 | result = self.instance.update_user_info(self.current_user, self.json) 41 | self.set_status(result.get("status_code", HTTPStatus.IM_A_TEAPOT)) 42 | return result 43 | 44 | @run_on_executor() 45 | def get_user_info(self) -> dict: 46 | username = self.get_current_user() 47 | if username: 48 | data = self.instance.get_user_info(username) 49 | else: 50 | # self.set_status(HTTPStatus.UNAUTHORIZED) 51 | self.clear_cookie("username") 52 | data = {"message": "Please try to login"} 53 | return data 54 | 55 | @gen.coroutine 56 | def post(self): 57 | resp = yield self.login() 58 | self.write(resp) 59 | 60 | @gen.coroutine 61 | def get(self): 62 | resp = yield self.get_user_info() 63 | self.write(resp) 64 | 65 | # everytime we receive a GET request to this api, we'll update last_date and last_ip 66 | username = self.get_current_user() 67 | if username: 68 | now_ip = self.get_real_ip() 69 | self.instance.update_user_last(username, now_ip) 70 | 71 | @gen.coroutine 72 | @web.authenticated 73 | def patch(self): 74 | resp = yield self.update_info() 75 | self.write(resp) 76 | 77 | 78 | class UserAvatarHandler(BaseHandler): 79 | filename = filename 80 | 81 | @run_on_executor() 82 | def update_avatar(self): 83 | username = self.get_current_user() 84 | if not username: 85 | self.set_status(HTTPStatus.UNAUTHORIZED) 86 | self.clear_cookie("username") 87 | return {"message": "Please try to login"} 88 | 89 | file = self.request.files["image"][0]["body"] 90 | if len(file) > 10 * 1024 * 1024: 91 | self.set_status(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) 92 | return {"message": "图片大小不可以超过10MB"} 93 | return self.instance.add_avatar(username, file) 94 | 95 | @run_on_executor() 96 | def get_avatar(self, username): 97 | user_hash = self.get_query_argument("hash", None) 98 | data = self.instance.get_avatar(username, user_hash) 99 | if data["image"]: 100 | self.set_header("Content-Type", data["content_type"]) 101 | return data["image"] 102 | self.set_status(HTTPStatus.NOT_FOUND) 103 | return b"" 104 | 105 | @gen.coroutine 106 | def post(self, _): 107 | resp = yield self.update_avatar() 108 | self.write(resp) 109 | 110 | @gen.coroutine 111 | def get(self, username): 112 | resp = yield self.get_avatar(username) 113 | self.write(resp) 114 | 115 | @gen.coroutine 116 | def head(self, username): 117 | resp = yield self.get_avatar(username) 118 | self.write(resp) 119 | 120 | 121 | class LikeHandler(BaseHandler): 122 | filename = filename 123 | 124 | @run_on_executor() 125 | def like_data(self): 126 | username = self.get_current_user() 127 | return {"LIKE": self.instance.get_user_like(username)} 128 | 129 | @gen.coroutine 130 | @web.authenticated 131 | def get(self): 132 | resp = yield self.like_data() 133 | self.write(resp) 134 | 135 | @run_on_executor() 136 | def add_remove_fav(self): 137 | data = self.json 138 | resource_id = int(data["resource_id"]) 139 | username = self.get_current_user() 140 | if username: 141 | response = self.instance.add_remove_fav(resource_id, username) 142 | self.set_status(response["status_code"]) 143 | else: 144 | response = {"message": "请先登录"} 145 | self.set_status(HTTPStatus.UNAUTHORIZED) 146 | 147 | return response["message"] 148 | 149 | @gen.coroutine 150 | @web.authenticated 151 | def patch(self): 152 | resp = yield self.add_remove_fav() 153 | self.write(resp) 154 | 155 | 156 | class UserEmailHandler(BaseHandler): 157 | filename = filename 158 | 159 | @run_on_executor() 160 | def verify_email(self): 161 | result = self.instance.verify_email(self.get_current_user(), self.json["code"]) 162 | self.set_status(result.get("status_code")) 163 | return result 164 | 165 | @gen.coroutine 166 | @web.authenticated 167 | def post(self): 168 | resp = yield self.verify_email() 169 | self.write(resp) 170 | -------------------------------------------------------------------------------- /yyetsweb/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding: utf-8 3 | 4 | # YYeTsBot - server.py 5 | # 2/5/21 21:02 6 | # 7 | 8 | __author__ = "Benny " 9 | 10 | import logging 11 | import os 12 | import pathlib 13 | import threading 14 | from zoneinfo import ZoneInfo 15 | 16 | import tornado.autoreload 17 | from apscheduler.schedulers.background import BackgroundScheduler 18 | from apscheduler.triggers.cron import CronTrigger 19 | from tornado import httpserver, ioloop, options, web 20 | from tornado.log import enable_pretty_logging 21 | 22 | from common.dump_db import entry_dump 23 | from common.sync import YYSub, sync_douban 24 | from common.utils import Cloudflare, setup_logger 25 | from databases.base import SearchEngine 26 | from databases.other import Other 27 | from handlers.base import IndexHandler, NotFoundHandler 28 | from handlers.comment import ( 29 | CommentChildHandler, 30 | CommentHandler, 31 | CommentNewestHandler, 32 | CommentReactionHandler, 33 | CommentSearchHandler, 34 | NotificationHandler, 35 | ) 36 | from handlers.douban import DoubanHandler, DoubanReportHandler 37 | from handlers.grafana import ( 38 | GrafanaIndexHandler, 39 | GrafanaQueryHandler, 40 | GrafanaSearchHandler, 41 | MetricsHandler, 42 | ) 43 | from handlers.oauth import ( 44 | FacebookAuth2LoginHandler, 45 | GitHubOAuth2LoginHandler, 46 | GoogleOAuth2LoginHandler, 47 | MSOAuth2LoginHandler, 48 | TwitterOAuth2LoginHandler, 49 | ) 50 | from handlers.other import ( 51 | AnnouncementHandler, 52 | BlacklistHandler, 53 | CaptchaHandler, 54 | CategoryHandler, 55 | DBDumpHandler, 56 | SpamProcessHandler, 57 | ) 58 | from handlers.resources import ( 59 | AdsenseStatusHandler, 60 | NameHandler, 61 | ResourceHandler, 62 | ResourceLatestHandler, 63 | SubtitleDownloadHandler, 64 | TopHandler, 65 | ) 66 | from handlers.user import LikeHandler, UserAvatarHandler, UserEmailHandler, UserHandler 67 | 68 | enable_pretty_logging() 69 | setup_logger() 70 | 71 | if os.getenv("debug"): 72 | logging.getLogger().setLevel(logging.DEBUG) 73 | 74 | 75 | class RunServer: 76 | static_path = pathlib.Path(__file__).parent.joinpath("templates") 77 | handlers = [ 78 | (r"/", IndexHandler), 79 | (r"/api/resource", ResourceHandler), 80 | (r"/api/download", SubtitleDownloadHandler), 81 | (r"/api/resource/latest", ResourceLatestHandler), 82 | (r"/api/top", TopHandler), 83 | (r"/api/like", LikeHandler), 84 | (r"/api/user", UserHandler), 85 | (r"/api/user/avatar/?(.*)", UserAvatarHandler), 86 | (r"/api/user/email", UserEmailHandler), 87 | (r"/api/name", NameHandler), 88 | (r"/api/adsense", AdsenseStatusHandler), 89 | (r"/api/comment", CommentHandler), 90 | (r"/api/comment/search", CommentSearchHandler), 91 | (r"/api/comment/reaction", CommentReactionHandler), 92 | (r"/api/comment/child", CommentChildHandler), 93 | (r"/api/comment/newest", CommentNewestHandler), 94 | (r"/api/captcha", CaptchaHandler), 95 | (r"/api/metrics", MetricsHandler), 96 | (r"/api/grafana/", GrafanaIndexHandler), 97 | (r"/api/grafana/search", GrafanaSearchHandler), 98 | (r"/api/grafana/query", GrafanaQueryHandler), 99 | (r"/api/blacklist", BlacklistHandler), 100 | (r"/api/db_dump", DBDumpHandler), 101 | (r"/api/announcement", AnnouncementHandler), 102 | (r"/api/douban", DoubanHandler), 103 | (r"/api/douban/report", DoubanReportHandler), 104 | (r"/api/notification", NotificationHandler), 105 | (r"/api/category", CategoryHandler), 106 | (r"/api/admin/spam", SpamProcessHandler), 107 | (r"/auth/github", GitHubOAuth2LoginHandler), 108 | (r"/auth/google", GoogleOAuth2LoginHandler), 109 | (r"/auth/twitter", TwitterOAuth2LoginHandler), 110 | (r"/auth/microsoft", MSOAuth2LoginHandler), 111 | (r"/auth/facebook", FacebookAuth2LoginHandler), 112 | ( 113 | r"/(.*\.html|.*\.js|.*\.css|.*\.png|.*\.jpg|.*\.ico|.*\.gif|.*\.woff2|.*\.gz|.*\.zip|" 114 | r".*\.svg|.*\.json|.*\.txt)", 115 | web.StaticFileHandler, 116 | {"path": static_path}, 117 | ), 118 | ] 119 | settings = { 120 | "cookie_secret": os.getenv("cookie_secret", "eo2kcgpKwXj8Q3PKYj6nIL1J4j3b58DX"), 121 | "default_handler_class": NotFoundHandler, 122 | "login_url": "/login", 123 | "google_oauth": { 124 | "key": os.getenv("GOOGLE_CLIENT_ID"), 125 | "secret": os.getenv("GOOGLE_CLIENT_SECRET"), 126 | }, 127 | "github_oauth": { 128 | "key": os.getenv("GITHUB_CLIENT_ID"), 129 | "secret": os.getenv("GITHUB_CLIENT_SECRET"), 130 | }, 131 | "ms_oauth": { 132 | "key": os.getenv("MS_CLIENT_ID"), 133 | "secret": os.getenv("MS_CLIENT_SECRET"), 134 | }, 135 | "fb_oauth": { 136 | "key": os.getenv("FB_CLIENT_ID"), 137 | "secret": os.getenv("FB_CLIENT_SECRET"), 138 | }, 139 | "twitter_consumer_key": os.getenv("TWITTER_CONSUMER_KEY"), 140 | "twitter_consumer_secret": os.getenv("TWITTER_CONSUMER_SECRET"), 141 | } 142 | application = web.Application(handlers, **settings) 143 | 144 | @staticmethod 145 | def run_server(port, host): 146 | tornado_server = httpserver.HTTPServer(RunServer.application, xheaders=True) 147 | tornado_server.bind(port, host) 148 | if os.getenv("PYTHON_DEV"): 149 | tornado_server.start(1) 150 | tornado.autoreload.start() 151 | else: 152 | tornado_server.start(0) 153 | 154 | try: 155 | print("Server is running on http://{}:{}".format(host, port)) 156 | ioloop.IOLoop.instance().current().start() 157 | except KeyboardInterrupt: 158 | ioloop.IOLoop.instance().stop() 159 | print('"Ctrl+C" received, exiting.\n') 160 | 161 | 162 | if __name__ == "__main__": 163 | timez = ZoneInfo("Asia/Shanghai") 164 | scheduler = BackgroundScheduler(timezone=timez) 165 | scheduler.add_job(Other().reset_top, trigger=CronTrigger.from_crontab("0 0 1 * *")) 166 | scheduler.add_job(sync_douban, trigger=CronTrigger.from_crontab("1 1 1 * *")) 167 | scheduler.add_job(entry_dump, trigger=CronTrigger.from_crontab("2 2 1 * *")) 168 | scheduler.add_job(Other().import_ban_user, "interval", seconds=300) 169 | scheduler.add_job(Other().fill_user_hash, "interval", seconds=60) 170 | scheduler.add_job(Cloudflare().clear_fw, trigger=CronTrigger.from_crontab("0 0 */3 * *")) 171 | scheduler.add_job(YYSub().run, trigger=CronTrigger.from_crontab("0 1 * * *")) 172 | 173 | scheduler.start() 174 | logging.info("Dumping database and ingesting data for Meilisearh...") 175 | if not os.getenv("PYTHON_DEV"): 176 | threading.Thread(target=entry_dump).start() 177 | # meilisearch tasks 178 | if os.getenv("MEILISEARCH"): 179 | logging.info("%s Searching with Meilisearch. %s", "#" * 10, "#" * 10) 180 | engine = SearchEngine() 181 | threading.Thread(target=engine.run_import).start() 182 | threading.Thread(target=engine.monitor_yyets).start() 183 | threading.Thread(target=engine.monitor_douban).start() 184 | threading.Thread(target=engine.monitor_comment).start() 185 | 186 | options.define("p", default=8888, help="running port", type=int) 187 | options.define("h", default="127.0.0.1", help="listen address", type=str) 188 | options.parse_command_line() 189 | p = options.options.p 190 | h = options.options.h 191 | banner = """ 192 | ▌ ▌ ▌ ▌ ▀▛▘ 193 | ▝▞ ▝▞ ▞▀▖ ▌ ▞▀▘ 194 | ▌ ▌ ▛▀ ▌ ▝▀▖ 195 | ▘ ▘ ▝▀▘ ▘ ▀▀ 196 | 197 | Lazarus came back from the dead. By @BennyThink 198 | """ 199 | print(banner) 200 | RunServer.run_server(port=p, host=h) 201 | -------------------------------------------------------------------------------- /yyetsweb/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | 33 | 人人影视下载分享 404 34 | 35 | 36 |
37 |

404 Not Found

38 |

你访问的页面不存在,将会在三秒钟内跳转回首页哦!

39 | a sad crab is unable to unable to lasso a paper airplane. 404 not found. 41 |
42 | 43 | 48 | 49 | -------------------------------------------------------------------------------- /yyetsweb/templates/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-2988254457061384, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /yyetsweb/templates/css/3rd/icons.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @IMPORT url("../font-awesome.min.css"); 3 | 4 | -------------------------------------------------------------------------------- /yyetsweb/templates/css/3rd/widgets.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | 4 | -------------------------------------------------------------------------------- /yyetsweb/templates/css/aYin.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /*webkit核心浏览器滚动条设置*/ 4 | /* 5 | ::-webkit-scrollbar{width: 10px;height:10px;} 6 | ::-webkit-scrollbar-track{border-radius: 0px;background: rgba(0,0,0,0.1);} 7 | ::-webkit-scrollbar-thumb{border-radius: 5px;background: rgba(0,0,0,0.2);} 8 | ::-webkit-scrollbar-thumb:hover{border-radius: 5px;background: rgba(0,0,0,0.4);} 9 | */ 10 | -------------------------------------------------------------------------------- /yyetsweb/templates/css/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "info": "OK", 4 | "data": { 5 | "info": { 6 | "id": 10060, 7 | "cnname": "铁皮人", 8 | "enname": "Tin Man", 9 | "aliasname": "", 10 | "channel": "tv", 11 | "channel_cn": "美剧", 12 | "area": "美国", 13 | "show_type": "", 14 | "expire": "1610371223", 15 | "views": "0" 16 | }, 17 | "list": [ 18 | { 19 | "season_num": "102", 20 | "season_cn": "MINI剧", 21 | "items": { 22 | "APP": [ 23 | { 24 | "itemid": "323953", 25 | "episode": "3", 26 | "name": "yyets://N=铁皮人.Tin.Man.Part3.中英字幕.BD.720p-人人影视.mp4|S=1327396629|H=1dd3a8b0ab765f4fd77f62e15b6635b932f5f9f7|", 27 | "size": "", 28 | "yyets_trans": 0, 29 | "dateline": "1493450710", 30 | "files": [ 31 | { 32 | "way": "102", 33 | "way_cn": "百度云", 34 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 35 | "passwd": "" 36 | }, 37 | { 38 | "way": "115", 39 | "way_cn": "微云", 40 | "address": "http://url.cn/5ZsPVGj", 41 | "passwd": "" 42 | } 43 | ] 44 | }, 45 | { 46 | "itemid": "323952", 47 | "episode": "2", 48 | "name": "yyets://N=铁皮人.Tin.Man.Part2.中英字幕.BD.720p-人人影视.mp4|S=1315079304|H=68edc943d6d9c67b0727c79e9b8531170559d459|", 49 | "size": "", 50 | "yyets_trans": 0, 51 | "dateline": "1493450699", 52 | "files": [ 53 | { 54 | "way": "102", 55 | "way_cn": "百度云", 56 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 57 | "passwd": "" 58 | }, 59 | { 60 | "way": "115", 61 | "way_cn": "微云", 62 | "address": "http://url.cn/5ZsPVGj", 63 | "passwd": "" 64 | } 65 | ] 66 | }, 67 | { 68 | "itemid": "323951", 69 | "episode": "1", 70 | "name": "yyets://N=铁皮人.Tin.Man.Part1.中英字幕.BD.720p-人人影视.mp4|S=1372733541|H=657ad3f2efd48057f5bea2e0ab111d9f429a785c|", 71 | "size": "", 72 | "yyets_trans": 0, 73 | "dateline": "1493450684", 74 | "files": [ 75 | { 76 | "way": "102", 77 | "way_cn": "百度云", 78 | "address": "https://pan.baidu.com/s/1rbm_tYCCWWj-Mn7GBwCP_g", 79 | "passwd": "" 80 | }, 81 | { 82 | "way": "115", 83 | "way_cn": "微云", 84 | "address": "http://url.cn/5ZsPVGj", 85 | "passwd": "" 86 | } 87 | ] 88 | } 89 | ], 90 | "MP4": [ 91 | { 92 | "itemid": "429977", 93 | "episode": "3", 94 | "name": "铁皮人.Tin.Man.Part3.中英字幕.BD.720p-人人影视.mp4", 95 | "size": "1.24GB", 96 | "yyets_trans": 0, 97 | "dateline": "1566559500", 98 | "files": [ 99 | { 100 | "way": "1", 101 | "way_cn": "电驴", 102 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part3.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1327396629|23b315a8d9e4a3f711494ccae4fc766e|h=mccx743p3l67dy7fqsrrqfhu4i6q3ohs|/", 103 | "passwd": "" 104 | }, 105 | { 106 | "way": "2", 107 | "way_cn": "磁力", 108 | "address": "magnet:?xt=urn:btih:fd23ad303b7121f91f56ecfd09faf0595a83c63e&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 109 | "passwd": "" 110 | }, 111 | { 112 | "way": "12", 113 | "way_cn": "诚通网盘", 114 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394367733", 115 | "passwd": "" 116 | } 117 | ] 118 | }, 119 | { 120 | "itemid": "429976", 121 | "episode": "2", 122 | "name": "铁皮人.Tin.Man.Part2.中英字幕.BD.720p-人人影视.mp4", 123 | "size": "1.22GB", 124 | "yyets_trans": 0, 125 | "dateline": "1566559500", 126 | "files": [ 127 | { 128 | "way": "1", 129 | "way_cn": "电驴", 130 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part2.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1315079304|7c602e1f24fd9f1fa65924eee0c4fd7d|h=mzg633nkorz7kmq53z2ecccnkchdle4m|/", 131 | "passwd": "" 132 | }, 133 | { 134 | "way": "2", 135 | "way_cn": "磁力", 136 | "address": "magnet:?xt=urn:btih:a40429e64c8e1a300f502045ddda8c74d45bd6a0&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 137 | "passwd": "" 138 | }, 139 | { 140 | "way": "12", 141 | "way_cn": "诚通网盘", 142 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394335534", 143 | "passwd": "" 144 | } 145 | ] 146 | }, 147 | { 148 | "itemid": "429975", 149 | "episode": "1", 150 | "name": "铁皮人.Tin.Man.Part1.中英字幕.BD.720p-人人影视.mp4", 151 | "size": "1.28GB", 152 | "yyets_trans": 0, 153 | "dateline": "1566559500", 154 | "files": [ 155 | { 156 | "way": "1", 157 | "way_cn": "电驴", 158 | "address": "ed2k://|file|%E9%93%81%E7%9A%AE%E4%BA%BA.Tin.Man.Part1.%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95.BD.720p-%E4%BA%BA%E4%BA%BA%E5%BD%B1%E8%A7%86.mp4|1372733541|58587c085040239c982ab8d7ff485515|h=nbd6y4jgdnwmh6ezarm2cfxiy5aw3qtp|/", 159 | "passwd": "" 160 | }, 161 | { 162 | "way": "2", 163 | "way_cn": "磁力", 164 | "address": "magnet:?xt=urn:btih:f2798104a57a380c0db44940ed180fad16930ede&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz", 165 | "passwd": "" 166 | }, 167 | { 168 | "way": "12", 169 | "way_cn": "诚通网盘", 170 | "address": "https://ZiMuZuUSTV.ctfile.com/fs/1939455-394335532", 171 | "passwd": "" 172 | } 173 | ] 174 | } 175 | ] 176 | }, 177 | "formats": [ 178 | "APP", 179 | "MP4" 180 | ] 181 | } 182 | ] 183 | } 184 | } -------------------------------------------------------------------------------- /yyetsweb/templates/css/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "越狱\nPrison Break\n逃": 10004, 3 | "死亡地带\nThe Dead Zone\n": 10010, 4 | "伪装者\nThe Pretender\n": 10017, 5 | "橘子郡男孩\nTHE OC\nThe.O.C / 橘镇风云 /桔子镇": 10022, 6 | "威尔和格蕾丝\nWill and Grace\n威尔与格蕾丝": 10026, 7 | "黑锅\nTraveler\n逃亡之旅": 10052, 8 | "棕榈泉疑云\nHidden Palms\n": 10053, 9 | "降世神通 最后的气宗\nAvatar: The Last Airbender\n": 10054, 10 | "恐怖大师\nMasters Of Horror\n": 10055, 11 | "时间旅人\nJourneyman\n时间旅者": 10057 12 | } -------------------------------------------------------------------------------- /yyetsweb/templates/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} 2 | /*# sourceMappingURL=normalize.min.css.map */ -------------------------------------------------------------------------------- /yyetsweb/templates/email_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 人人影视下载站 7 | 100 | 101 | 102 | 104 | 107 | 108 | 109 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /yyetsweb/templates/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/favicon.ico -------------------------------------------------------------------------------- /yyetsweb/templates/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /yyetsweb/templates/googlee927c64a054a7beb.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlee927c64a054a7beb.html -------------------------------------------------------------------------------- /yyetsweb/templates/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 人人影视下载分享 - 常见问题 9 | 41 | 42 | 43 |
44 |

人人影视下载分享 FAQ

45 |
46 |
47 |

1. 如何下载

48 | 一般来说,分享页面的下载链接是有电驴、磁力链和网盘的。网盘就去下载对应的客户端,电驴和磁力链,可以尝试下迅雷。
49 | 磁力链还可以试试uTorrent、BitCommet,并且记得去同步一份最新的tracker哦。
50 | 另外值得一提的是,有些网盘支持离线资源,可以把磁力、ed2k贴进去。 51 |

2. 8天后删除是什么意?

52 | 假的,再刷新就满血复活了 53 |

3. 是否会关站

54 | 不会,我的主观意愿不会。即使被关站也不要怕,本项目的代码、数据库都是开源的。 55 |

4. 网站能承受住多大流量

56 | 我的测试,至少500请求/second。服务器流量多着呢,谁怕谁啊。 57 |

5. 为什么我的资源结果页是一片空白

58 | 这个,按理来说不应该啊,遇到这种问题去GitHub发issue、私聊或者邮件告诉我吧。 59 |

6. 想要一份备份

60 | 好的,请去GitHub! 61 |

7. 开源地址有吗

62 | 有的,GitHub,数据库也有 63 |

8. 页面太丑,能优化下吗

64 | 😂……难为我,我真不怎么会前端。不过欢迎提PR 65 |

9. 有些资源没有,是什么原因。

66 | 这个是做的归档数据,所以新剧是没有的。当然,如果我心情好,也许还会手动添加几部。 67 |

10. 搜索没结果,怎么回事?

68 | 是不是译名和人人影视那边不一样?试试原名?尽量缩短关键词? 69 |

11. 想要支持我

70 | 谢谢你哦!可以通过点🌟,宣传,使用等方式来支持。当然你也可以通过 71 | Buy Me a Coffee 72 | 或 73 | 爱发电 74 | 来支持我! 75 |

12. 其他的使用方式

76 | 还有一个Telegram Bot,数据和这个网站是同步的。参考页脚 77 |

13. 可以商业化这个项目吗

78 | 有一点需要先声明,我不会进行如加入会员制度等商业化行为。所有用户等捐赠是自发性并且不具有强制性。
79 | 本项目使用MIT协议授权,因此你可以进行商业化,以任意形式进行分发、修改以及私有使用。你只要保持保持版权声明就可以了。 80 |

14. 网站打不开,无法搜索

81 | 呃这个应该不太可能啊。可能你网络连接到cloudflare不太稳?换个网络,再刷新试试看呢?还不行去GitHub报错给我吧。 82 |

15. 本站会收集哪些信息

83 | 主要有如下4种信息:
84 | 1. 你的IP地址会被记录在Nginx的日志中。在面临攻击、爬虫等恶意行为时,我会找到这个IP然后加到防火墙中。我不会公开或与第三方分享访问日志;
85 | 2. 我使用了Google Analytics,请参考Google Analytics的隐私政策;
86 | 3. 我使用了Cloudflare,请参考Cloudflare的隐私政策。
87 | 4. 我记录了metrics信息,用于优化日后访问量,此信息不包含个人信息,无法用于追踪你。可以点击这里查看
88 | 5. 如果你选择注册,我会保存你的用户名、加密后的密码、注册时间、UA等信息。此类信息不会被公开或与第三方分享。
89 | 6. 如果你选择收藏某些资源,那么日后可能用于统计排行榜,这项信息不会包含你的个人信息。毕竟,大家都喜欢,那才是最棒的嘛! 90 |

16. 数据库下载不了呀!

91 | 那你试试这几个呢?sha1 checksum供参考
92 | MongoDB 93 | {{ data["data/yyets_mongo.gz"][0] }} 更新时间{{ data["data/yyets_mongo.gz"][1] }} 94 |
95 | MySQL 5.7 96 | {{ data["data/yyets_mongo.gz"][0] }} 更新时间{{ data["data/yyets_mysql.zip"][1] }} 97 |
98 | SQLite 99 | {{ data["data/yyets_mongo.gz"][0] }} 更新时间{{ data["data/yyets_sqlite.zip"][1] }} 100 |
101 | 102 |

17. 注册需要提供什么?

103 | 呃,其实随便写个用户名密码就可以了,没有限制的哦。我使用了 pbkdf2_sha256 安全保存你的密码。
104 | 我知道这个功能太简陋了,比如说无法找回密码,很多很多。凑合用吧😂
105 | 哦对了,你的用户信息不会被包含在上述数据库之中。 106 |
107 |
108 | 109 |
110 |
111 |
112 |

相关资源⬇️

113 |

114 | 联系我 Benny 小可爱・ 115 | YYeTs 机器人Telegram Bot・ 116 | Telegram Channel・ 117 | 开源 GitHub・ 118 | 我的博客 土豆不好吃 119 |

120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /yyetsweb/templates/img/11bcd4d0f2daf8b02fecc72bc8ca38ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/11bcd4d0f2daf8b02fecc72bc8ca38ab.png -------------------------------------------------------------------------------- /yyetsweb/templates/img/200-wrangler-ferris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/200-wrangler-ferris.gif -------------------------------------------------------------------------------- /yyetsweb/templates/img/404-wrangler-ferris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/404-wrangler-ferris.gif -------------------------------------------------------------------------------- /yyetsweb/templates/img/afdian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/afdian.png -------------------------------------------------------------------------------- /yyetsweb/templates/img/default-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/default-green.png -------------------------------------------------------------------------------- /yyetsweb/templates/img/grid16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/grid16.png -------------------------------------------------------------------------------- /yyetsweb/templates/img/yyetsTrans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgbot-collection/YYeTsBot/518de74dc4a3e106812dd622dc4435d1663a9e34/yyetsweb/templates/img/yyetsTrans.png -------------------------------------------------------------------------------- /yyetsweb/templates/js/common.js: -------------------------------------------------------------------------------- 1 | function getCookie(cname) { 2 | var name = cname + "="; 3 | var ca = document.cookie.split(';'); 4 | for (var i = 0; i < ca.length; i++) { 5 | var c = ca[i].trim(); 6 | if (c.indexOf(name) === 0) return c.substring(name.length, c.length); 7 | } 8 | return ""; 9 | } 10 | 11 | 12 | function accessMetrics(type) { 13 | axios.post('/api/metrics?type=' + type) 14 | .then(function (response) { 15 | // console.log(response); 16 | }) 17 | .catch(function (error) { 18 | // console.log(error); 19 | }); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /yyetsweb/templates/js/jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using UglifyJS v3.4.4. 3 | * Original file: /npm/jquery-mousewheel@3.1.13/jquery.mousewheel.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(d){var c,m,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in document||9<=document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],g=Array.prototype.slice;if(d.event.fixHooks)for(var i=e.length;i;)d.event.fixHooks[e[--i]]=d.event.mouseHooks;var w=d.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],n,!1);else this.onmousewheel=n;d.data(this,"mousewheel-line-height",w.getLineHeight(this)),d.data(this,"mousewheel-page-height",w.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],n,!1);else this.onmousewheel=null;d.removeData(this,"mousewheel-line-height"),d.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=d(e),i=t["offsetParent"in d.fn?"offsetParent":"parent"]();return i.length||(i=d("body")),parseInt(i.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return d(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function n(e){var t,i=e||window.event,n=g.call(arguments,1),o=0,l=0,s=0,a=0,h=0;if((e=d.event.fix(i)).type="mousewheel","detail"in i&&(s=-1*i.detail),"wheelDelta"in i&&(s=i.wheelDelta),"wheelDeltaY"in i&&(s=i.wheelDeltaY),"wheelDeltaX"in i&&(l=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in i&&(o=s=-1*i.deltaY),"deltaX"in i&&(l=i.deltaX,0===s&&(o=-1*l)),0!==s||0!==l){if(1===i.deltaMode){var r=d.data(this,"mousewheel-line-height");o*=r,s*=r,l*=r}else if(2===i.deltaMode){var u=d.data(this,"mousewheel-page-height");o*=u,s*=u,l*=u}if(t=Math.max(Math.abs(s),Math.abs(l)),(!m||t 2 | 3 | 4 | 5 | 6 | 17 | 18 | 人人影视下载分享 19 | 20 | 21 | 22 | 48 | 49 | 50 |
51 |

人人影视下载分享 By Benny

52 |

53 | Buy Me A Coffee 56 | 爱发电 58 |

59 |

本站数据库永久开源免费请不要做无意义的爬虫。 61 | 使用帮助? 62 |

63 |
64 | 67 | 68 |
69 | 70 |
71 |
72 |
73 |
74 | 75 |

76 | 联系我 Benny 小可爱・ 77 | YYeTs 机器人Telegram Bot・ 78 | 开源 GitHub・ 79 | Telegram Channel・ 80 | 我的博客 土豆不好吃 81 |

82 |
83 | 84 | 85 | 86 | 87 | 133 | 134 | -------------------------------------------------------------------------------- /yyetsweb/tests/router_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | import pathlib 5 | import sys 6 | import unittest 7 | 8 | from tornado.testing import AsyncHTTPTestCase 9 | 10 | sys.path.append(pathlib.Path(__file__).parent.parent.as_posix()) 11 | from server import RunServer 12 | 13 | 14 | class YYeTsTest(AsyncHTTPTestCase): 15 | def get_app(self): 16 | return RunServer.application 17 | 18 | 19 | class TestIndex(YYeTsTest): 20 | def test_homepage(self): 21 | response = self.fetch("/") 22 | self.assertEqual(response.code, 200) 23 | self.assertTrue(b"" in response.body) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | --------------------------------------------------------------------------------