├── .github └── workflows │ └── maven-publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── doc ├── EditThisCookie.png └── ExportCookie.png ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── hsn │ │ │ └── epic4j │ │ │ ├── boot │ │ │ ├── Epic4jApplication.java │ │ │ ├── MultiUserRunner.java │ │ │ ├── SingleUserRunner.java │ │ │ └── SpringEpicConfig.java │ │ │ └── core │ │ │ ├── EpicStarter.java │ │ │ ├── ILogin.java │ │ │ ├── IStart.java │ │ │ ├── IUpdate.java │ │ │ ├── MainStart.java │ │ │ ├── MavenUpdate.java │ │ │ ├── PasswordLogin.java │ │ │ ├── Retry.java │ │ │ ├── StartProxy.java │ │ │ ├── ThreadContext.java │ │ │ ├── UrlConstants.java │ │ │ ├── WatchDogThread.java │ │ │ ├── bean │ │ │ ├── AliMvnDto.java │ │ │ ├── CatalogNs.java │ │ │ ├── Item.java │ │ │ ├── PageSlug.java │ │ │ ├── SelectItem.java │ │ │ └── UserInfo.java │ │ │ ├── config │ │ │ └── EpicConfig.java │ │ │ ├── exception │ │ │ ├── CheckException.java │ │ │ ├── ItemException.java │ │ │ ├── OMSException.java │ │ │ ├── PermissionException.java │ │ │ └── TimeException.java │ │ │ ├── notify │ │ │ ├── ConsoleNotify.java │ │ │ └── INotify.java │ │ │ └── util │ │ │ ├── PageUtil.java │ │ │ ├── ScreenShootUtil.java │ │ │ ├── SelectorUtil.java │ │ │ └── VersionCompare.java │ └── resources │ │ ├── application.yml │ │ └── banner.txt └── test │ └── java │ └── com │ └── hsn │ └── epic4j │ ├── CrawTest.java │ └── Epic4jApplicationTests.java └── start.sh /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | # 声明 checkout 仓库代码到工作区 20 | - name: Checkout Git Repo 21 | uses: actions/checkout@v2 22 | - name: get-pom-version 23 | id: pom-version 24 | uses: CptMokoena/maven-get-version-action@main 25 | # 安装Java 环境 这里会用到的参数就是 Git Action secrets中配置的, 26 | # 取值要在key前面加 secrets. 27 | - name: Set up Maven Central Repo 28 | uses: actions/setup-java@v1 29 | with: 30 | java-version: 1.8 31 | server-id: sonatype-nexus-staging 32 | server-username: ${{ secrets.OSSRH_USERNAME }} 33 | server-password: ${{ secrets.OSSRH_PASSWORD }} 34 | gpg-passphrase: ${{ secrets.GPG_PASSWORD }} 35 | # 3. 发布到Maven中央仓库 36 | - name: Publish to Maven Central Repo 37 | # 这里用到了其他人写的action脚本,详细可以去看他的文档。 38 | uses: samuelmeuli/action-maven-publish@v1 39 | with: 40 | gpg_private_key: ${{ secrets.GPG_SECRET }} 41 | gpg_passphrase: ${{ secrets.GPG_PASSWORD }} 42 | nexus_username: ${{ secrets.OSSRH_USERNAME }} 43 | nexus_password: ${{ secrets.OSSRH_PASSWORD }} 44 | maven_goals_phases: clean deploy -DskipTests 45 | # 设置qemu来编译多个平台的镜像 46 | - name: Set up QEMU 47 | uses: docker/setup-qemu-action@v1 48 | # 设置buildx 49 | - name: Set up Docker Buildx 50 | uses: docker/setup-buildx-action@v1 51 | #登录docker hub 52 | - name: Login to DockerHub 53 | uses: docker/login-action@v1 54 | with: 55 | username: ${{ secrets.DOCKERHUB_USERNAME }} 56 | password: ${{ secrets.DOCKERHUB_TOKEN }} 57 | - name: Build and push 58 | uses: docker/build-push-action@v2 59 | with: 60 | context: . 61 | platforms: linux/amd64 #,linux/arm64 62 | push: true 63 | tags: huisunan/epic4j:latest, huisunan/epic4j:${{ steps.pom-version.outputs.version }} 64 | #release 65 | # - name: Upload release 66 | # uses: softprops/action-gh-release@v1 67 | # with: 68 | # files: target/epic4j.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | /data/ 35 | .local-browser/ 36 | epic4j.jar.update -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | MAINTAINER huisunan 3 | ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable 4 | WORKDIR /opt/epic4j 5 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 78BD65473CB3BD13 \ 6 | && sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \ 7 | && apt-get update \ 8 | && apt-get install google-chrome-stable -y 9 | COPY target/epic4j.jar ./ 10 | COPY start.sh ./ 11 | CMD /opt/epic4j/start.sh 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2022] [huisunan] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Epic4j 2 | 3 | 4 | # [一个更好的游戏领取项目](https://github.com/QIN2DIM/epic-awesome-gamer)推荐大家使用 5 | 6 | ## 本项目可能要暂停维护了 7 | 8 | 9 | [Epic4j](https://github.com/huisunan/epic4j)|[EpicGamesClaimer](https://github.com/luminoleon/epicgames-claimer) 10 | 11 | > 免费领取Epic周免游戏,本项目由EpicGamesClaimer而来 12 | 13 | QQ交流群:551322748 14 | 15 | ## 开始 16 | 17 | ### window 18 | 19 | #### ide下运行 20 | 21 | 需要环境 jdk8+,maven 拉取项目编译后运行 22 | 23 | #### 命令行运行 24 | 25 | [下载jar包](https://github.com/huisunan/epic4j/releases) 26 | 27 | ```shell 28 | java -jar -Depic.email=[你的账号] -Depic.password[你的密码] epic4j.jar 29 | ``` 30 | 使用cookie 31 | ```shell 32 | java -jar -Depic.email=[你的账号] -Depic.password[你的密码] -Depic.cookiePath=[你的cookie路径] epic4j.jar 33 | ``` 34 | 35 | ### Docker 36 | 37 | ```shell 38 | #docker拉取 39 | docker pull huisunan/epic4j:latest 40 | #密码登录 41 | docker run -d -e EMAIL=[你的邮箱] -e PASSWORD=[你的密码] --name epic4j huisunan/epic4j:latest 42 | #debug模式运行 43 | docker run -d -e EMAIL=[你的邮箱] -e PASSWORD=[你的密码] -e LOG_LEVEL=debug --name epic4j huisunan/epic4j:latest 44 | #cookie登录 45 | docker run -d -e EMAIL=[你的邮箱] -e PASSWORD=[你的密码] -e COOKIE_PATH=[cookie路径] -v [本机cookie路径]:[cookie路径] --name epic4j huisunan/epic4j:latest 46 | 47 | ``` 48 | 49 | **挂载配置文件方式运行(推荐)** 50 | 51 | [具体配置](#yaml) 52 | 53 | ```shell 54 | # 创建数据目录 55 | mkdir ~/epic4j 56 | # 创建配置文件 57 | vim ~/epic4j/application.yml 58 | # 创建持久卷,用来保存用户数据,再升级容器时保存用户数据 59 | docker volume create epic4jVolume 60 | ``` 61 | 62 | application.yml的配置如下 63 | 64 | ```yaml 65 | epic: 66 | email: 你的邮箱 67 | password: 你的密码 68 | #开启自动更新,可选 69 | auto-update: true 70 | ``` 71 | 72 | 运行docker容器,挂载配置文件到/opt/epic4j/config下 73 | 74 | ```shell 75 | docker run -d -v ~/epic4j:/opt/epic4j/config -v epic4jVolume:/opt/epic4j/data --name myepic huisunan/epic4j:latest 76 | ``` 77 | 78 | #### 多用户配置 79 | 80 | 以上为单用户配置,还支持多用户配置 81 | 82 | ```yaml 83 | epic: 84 | #开启自动更新,可选 85 | auto-update: true 86 | # 开启多用户支持 87 | multi-user: true 88 | users: 89 | - email: demo1 90 | password: pass1 91 | - email: demo2 92 | password: pass2 93 | ``` 94 | 95 | ## 配置 96 | 97 | ### yaml 98 | 99 | 其中的参数值为默认值 100 | 101 | ```yaml 102 | epic: 103 | # 浏览器用户文件存储位置,默认为jar包同路径下data文件夹,不存在会新建目录 104 | dataPath: ./data 105 | # 浏览器启动参数 106 | driverArgs: 107 | # email邮箱地址 108 | email: 109 | # 密码 110 | password: password 111 | # headLess无头模式 112 | headLess: true 113 | # browserVersion指定chromium的版本,可能有一定风险 114 | browser-version: 115 | # crontab表达式,不填写的情况下是每天程序启动的时分秒运行一次 116 | cron: 117 | # noSandbox非沙盒运行 118 | no-sandbox: true 119 | # cookie cookie路径,如果路径不为空会加载cookie 120 | cookie-path: 121 | # 自动更新默认为false,true开启 122 | auto-update: false 123 | # 开启多用户 默认为false 124 | multi-user: false 125 | # 多用户信息 126 | users: 127 | # 错误时截图,默认为true 128 | error-screen-shoot: true 129 | # 操作超时时间ms,默认30s 130 | timeout: 30000 131 | # 操作间隔ms,间隔越短,轮询越快,适当控制 132 | interval: 100 133 | ``` 134 | 135 | ### 环境变量 136 | 137 | 可以配置的环境变量 138 | 139 | | 参数 | 说明 | 备注 | 140 | | ---- | ---- | ----- | 141 | |EMAIL|邮箱地址|| 142 | |PASSWORD|密码|| 143 | |LOG_LEVEL|日志级别|日志级别为debug可以看到更多的日志| 144 | |COOKIE_PATH|cookie路径|cookie不为空则加载,docker下通过挂载目录的方式,加载cookie路径| 145 | |CRON|cron表达式|定时任务([表达式验证](https://www.bejson.com/othertools/cronvalidate/))| 146 | 147 | ## 计划 148 | 149 | |名称|状态| 150 | |---|----| 151 | |cookie登录|✅| 152 | |i18n支持|| 153 | |消息推送|| 154 | |自动更新|✅| 155 | |多账号批量处理|✅| 156 | |可视化界面|| 157 | 158 | ## 获取cookie 159 | 使用chrome浏览器安装[EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg) 160 | ![](doc/EditThisCookie.png) 161 | 162 | 获取网站的cookie 163 | ![](doc/ExportCookie.png) 164 | 165 | 新建文本文件保存cookie 166 | -------------------------------------------------------------------------------- /doc/EditThisCookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huisunan/epic4j/148e851f66f22dbec27f52e1cd6cf831a175035a/doc/EditThisCookie.png -------------------------------------------------------------------------------- /doc/ExportCookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huisunan/epic4j/148e851f66f22dbec27f52e1cd6cf831a175035a/doc/ExportCookie.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.2 9 | 10 | 11 | io.github.huisunan 12 | epic4j 13 | 1.5.0 14 | epic4j 15 | epic4j 16 | 17 | 1.8 18 | 19 | 20 | 21 | huisunan 22 | Freedom 23 | https://github.com/huisunan 24 | 25 | 26 | 27 | 28 | Apache 2 29 | http://www.apache.org/licenses/LICENSE-2.0.txt 30 | repo 31 | A business-friendly OSS license 32 | 33 | 34 | 35 | https://github.com/huisunan 36 | https://github.com/huisunan/epic4j.git 37 | 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter 43 | 44 | 45 | 46 | org.projectlombok 47 | lombok 48 | true 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-configuration-processor 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-test 57 | test 58 | 59 | 60 | io.github.fanyong920 61 | jvppeteer 62 | 1.1.4 63 | 64 | 65 | cn.hutool 66 | hutool-all 67 | 5.7.18 68 | 69 | 70 | 71 | 72 | 73 | 74 | ossrh 75 | https://s01.oss.sonatype.org/content/repositories/snapshots 76 | 77 | 78 | ossrh 79 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 80 | 81 | 82 | 83 | 84 | epic4j 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-maven-plugin 89 | 90 | 91 | 92 | org.projectlombok 93 | lombok 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-source-plugin 102 | 2.2.1 103 | 104 | 105 | attach-sources 106 | 107 | jar-no-fork 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-javadoc-plugin 115 | 2.9.1 116 | 117 | 118 | attach-javadocs 119 | 120 | jar 121 | 122 | 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-gpg-plugin 128 | 1.5 129 | 130 | 131 | sign-artifacts 132 | verify 133 | 134 | sign 135 | 136 | 137 | 138 | 139 | --pinentry-mode 140 | loopback 141 | 142 | 143 | 144 | 145 | 146 | 147 | org.sonatype.plugins 148 | nexus-staging-maven-plugin 149 | 1.6.7 150 | true 151 | 152 | ossrh 153 | https://s01.oss.sonatype.org/ 154 | true 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/boot/Epic4jApplication.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.boot; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Epic4jApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Epic4jApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/boot/MultiUserRunner.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.boot; 2 | 3 | import com.hsn.epic4j.core.EpicStarter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.ApplicationArguments; 7 | import org.springframework.boot.ApplicationRunner; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * @author hsn 13 | * 2022/1/6 14 | * MultiUserRunner 15 | */ 16 | @Slf4j 17 | @Component 18 | @ConditionalOnProperty(prefix = "epic", name = "multi-user", havingValue = "true") 19 | public class MultiUserRunner implements ApplicationRunner { 20 | @Autowired 21 | private SpringEpicConfig epicConfig; 22 | 23 | @Override 24 | public void run(ApplicationArguments args) throws Exception { 25 | EpicStarter.withConfig(epicConfig, epicConfig.getUsers()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/boot/SingleUserRunner.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.boot; 2 | 3 | import com.hsn.epic4j.core.EpicStarter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.ApplicationArguments; 7 | import org.springframework.boot.ApplicationRunner; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.Collections; 12 | 13 | @Slf4j 14 | @Component 15 | @ConditionalOnProperty(prefix = "epic", name = "multi-user", havingValue = "false") 16 | public class SingleUserRunner implements ApplicationRunner { 17 | 18 | @Autowired 19 | private SpringEpicConfig epicConfig; 20 | 21 | 22 | @Override 23 | public void run(ApplicationArguments args) throws InterruptedException { 24 | EpicStarter.withConfig(epicConfig,Collections.singletonList(epicConfig)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/boot/SpringEpicConfig.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.boot; 2 | 3 | import com.hsn.epic4j.core.config.EpicConfig; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @EqualsAndHashCode(callSuper = true) 10 | @Data 11 | @Configuration 12 | @ConfigurationProperties(prefix = "epic") 13 | public class SpringEpicConfig extends EpicConfig { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/EpicStarter.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import cn.hutool.core.date.DateField; 4 | import cn.hutool.core.date.DateTime; 5 | import cn.hutool.core.date.DateUtil; 6 | import cn.hutool.core.io.FileUtil; 7 | import cn.hutool.core.io.IoUtil; 8 | import cn.hutool.core.lang.TypeReference; 9 | import cn.hutool.core.util.StrUtil; 10 | import cn.hutool.json.JSONUtil; 11 | import com.hsn.epic4j.core.bean.Item; 12 | import com.hsn.epic4j.core.bean.UserInfo; 13 | import com.hsn.epic4j.core.config.EpicConfig; 14 | import com.hsn.epic4j.core.notify.ConsoleNotify; 15 | import com.hsn.epic4j.core.notify.INotify; 16 | import com.hsn.epic4j.core.util.PageUtil; 17 | import com.hsn.epic4j.core.util.ScreenShootUtil; 18 | import com.ruiyun.jvppeteer.core.browser.Browser; 19 | import com.ruiyun.jvppeteer.core.page.Page; 20 | import com.ruiyun.jvppeteer.protocol.network.CookieParam; 21 | import lombok.Data; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.springframework.core.io.FileUrlResource; 24 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 25 | import org.springframework.scheduling.support.CronTrigger; 26 | 27 | import java.io.File; 28 | import java.io.FileReader; 29 | import java.lang.reflect.Proxy; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.stream.Collectors; 33 | 34 | /** 35 | * 用于一次运行一次领取游戏 36 | */ 37 | @Slf4j 38 | @Data 39 | public class EpicStarter { 40 | private final EpicConfig epicConfig; 41 | private final IStart iStart; 42 | private final ILogin iLogin; 43 | private final IUpdate update; 44 | private final List userInfos; 45 | 46 | 47 | private ThreadPoolTaskScheduler scheduler; 48 | private List notifies = new ArrayList<>(); 49 | 50 | 51 | public EpicStarter(EpicConfig epicConfig, IStart iStart, ILogin iLogin, IUpdate update, List userInfos) { 52 | this.epicConfig = epicConfig; 53 | //处理代理 54 | StartProxy startProxy = new StartProxy(iStart); 55 | this.iStart = (IStart) Proxy.newProxyInstance(iStart.getClass().getClassLoader(), iStart.getClass().getInterfaces(), startProxy); 56 | this.iLogin = iLogin; 57 | this.update = update; 58 | this.userInfos = userInfos; 59 | this.notifies.add(new ConsoleNotify()); 60 | this.initCron(); 61 | } 62 | 63 | public static EpicStarter withConfig(EpicConfig epicConfig, List userInfos) { 64 | return new EpicStarter( 65 | epicConfig, 66 | new MainStart(epicConfig), 67 | new PasswordLogin(), 68 | new MavenUpdate(epicConfig.getVersion()), 69 | userInfos 70 | ); 71 | } 72 | 73 | protected void checkForUpdate() { 74 | if (epicConfig.getAutoUpdate()) { 75 | update.checkForUpdate(); 76 | } 77 | } 78 | 79 | /** 80 | * 初始化cron 81 | */ 82 | protected void initCron() { 83 | String cronExpression = epicConfig.getCron(); 84 | boolean notCron = StrUtil.isBlank(cronExpression); 85 | if (notCron) { 86 | DateTime now = DateUtil.date().offset(DateField.SECOND, 3); 87 | cronExpression = StrUtil.format("{} {} {} * * ?", now.second(), now.minute(), now.hour(true)); 88 | } 89 | log.info("use cron:{}", cronExpression); 90 | scheduler = new ThreadPoolTaskScheduler(); 91 | scheduler.initialize(); 92 | String finalCronExpression = cronExpression; 93 | scheduler.schedule(this::start, context -> new CronTrigger(finalCronExpression).nextExecutionTime(context)); 94 | //立即执行一次 95 | try { 96 | if (!notCron) 97 | start(); 98 | } catch (Exception e) { 99 | log.error("立即执行出错", e); 100 | } 101 | } 102 | 103 | protected void start() { 104 | checkForUpdate(); 105 | //获取周免游戏 106 | List weekFreeItems = iStart.getFreeItems().stream().peek(i -> { 107 | if (StrUtil.endWith(i.getProductSlug(), "/home")) { 108 | i.setProductSlug(i.getProductSlug().replace("/home", "")); 109 | } 110 | }).collect(Collectors.toList()); 111 | //处理 productSlug带/home的清空 112 | for (UserInfo info : userInfos) { 113 | doStart(info, weekFreeItems); 114 | } 115 | } 116 | 117 | public void doStart(UserInfo userInfo, List weekFreeItems) { 118 | Browser browser = null; 119 | WatchDogThread watchDogThread = null; 120 | try { 121 | ThreadContext.init(epicConfig.getTimeout(), epicConfig.getInterval()); 122 | String desensitizedEmail = email(userInfo.getEmail()); 123 | log.info("账号[{}]开始工作", desensitizedEmail); 124 | //用户文件路径 125 | String userDataPath = new FileUrlResource(epicConfig.getDataPath() + File.separator + userInfo.getEmail()).getFile().getAbsolutePath(); 126 | log.debug("用户数据目录:{}", userDataPath.replace(userInfo.getEmail(), desensitizedEmail)); 127 | //获取浏览器对象 128 | browser = iStart.getBrowser(userDataPath); 129 | //获取默认page 130 | Page page = iStart.getDefaultPage(browser); 131 | //加载cookie 132 | if (StrUtil.isNotBlank(epicConfig.getCookiePath()) && FileUtil.exist(epicConfig.getCookiePath())) { 133 | log.debug("加载cookie"); 134 | List cookies = JSONUtil.toBean(IoUtil.read(new FileReader(epicConfig.getCookiePath())), 135 | new TypeReference>() { 136 | }, false); 137 | page.setCookie(cookies); 138 | } 139 | //反爬虫设置 140 | PageUtil.crawSet(page); 141 | watchDogThread = new WatchDogThread(browser, Thread.currentThread(), epicConfig.getHeadLess()); 142 | watchDogThread.start(); 143 | //打开epic主页 144 | iStart.goToEpic(page); 145 | List pages = browser.pages(); 146 | for (Page p : pages) { 147 | if (!StrUtil.startWith(p.mainFrame().url(), "http")) { 148 | p.close(); 149 | } 150 | } 151 | boolean needLogin = iStart.needLogin(page); 152 | log.debug("是否需要登录:{}", needLogin ? "是" : "否"); 153 | if (needLogin) { 154 | iLogin.login(page, userInfo.getEmail(), userInfo.getPassword()); 155 | } 156 | //领取游戏 157 | List receive = iStart.receive(page, weekFreeItems); 158 | for (INotify notify : notifies) { 159 | if (notify.notifyReceive(receive)) { 160 | break; 161 | } 162 | } 163 | } catch (Exception e) { 164 | if (epicConfig.getErrorScreenShoot()) { 165 | //输出截图和html信息 166 | long currentTimeMillis = System.currentTimeMillis(); 167 | ScreenShootUtil.screen(browser, "data/error", currentTimeMillis + ".jpeg", currentTimeMillis + ".html"); 168 | } 169 | log.error("程序异常", e); 170 | } finally { 171 | if (watchDogThread != null) { 172 | watchDogThread.interrupt(); 173 | } 174 | if (browser != null) { 175 | browser.close(); 176 | } 177 | ThreadContext.clear(); 178 | } 179 | } 180 | 181 | protected String email(String email) { 182 | if (StrUtil.isBlank(email)) { 183 | return StrUtil.EMPTY; 184 | } 185 | int index = StrUtil.indexOf(email, '@'); 186 | if (index <= 3) { 187 | return email; 188 | } 189 | return StrUtil.hide(email, 3, index); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/ILogin.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import com.ruiyun.jvppeteer.core.page.Page; 4 | 5 | public interface ILogin { 6 | void login(Page page, String email, String password); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/IStart.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import com.hsn.epic4j.core.bean.Item; 4 | import com.ruiyun.jvppeteer.core.browser.Browser; 5 | import com.ruiyun.jvppeteer.core.page.Page; 6 | 7 | import java.util.List; 8 | 9 | public interface IStart { 10 | /** 11 | * 获取浏览器 12 | */ 13 | Browser getBrowser(String dataPath); 14 | 15 | /** 16 | * 获取默认页面 17 | */ 18 | Page getDefaultPage(Browser browser); 19 | 20 | /** 21 | * 判断是否要登录 22 | */ 23 | boolean needLogin(Page page); 24 | 25 | /** 26 | * 领取游戏 27 | */ 28 | List receive(Page page, List weekFreeItems); 29 | 30 | /** 31 | * 获取免费游戏 32 | */ 33 | List getFreeItems(); 34 | 35 | /** 36 | * 跳转到epic 37 | */ 38 | void goToEpic(Page page); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/IUpdate.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | /** 4 | * @author hsn 5 | * 2022/1/4 6 | * IUpdate 7 | */ 8 | public interface IUpdate { 9 | String CONFIG = "update-type"; 10 | int UPDATE_EXIT_CODE = 66; 11 | 12 | void checkForUpdate(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/MainStart.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import cn.hutool.core.collection.CollUtil; 4 | import cn.hutool.core.date.DateTime; 5 | import cn.hutool.core.date.DateUtil; 6 | import cn.hutool.core.util.StrUtil; 7 | import cn.hutool.http.HttpUtil; 8 | import cn.hutool.json.JSONArray; 9 | import cn.hutool.json.JSONObject; 10 | import cn.hutool.json.JSONUtil; 11 | import com.hsn.epic4j.core.bean.Item; 12 | import com.hsn.epic4j.core.bean.PageSlug; 13 | import com.hsn.epic4j.core.bean.SelectItem; 14 | import com.hsn.epic4j.core.config.EpicConfig; 15 | import com.hsn.epic4j.core.exception.ItemException; 16 | import com.hsn.epic4j.core.exception.PermissionException; 17 | import com.hsn.epic4j.core.exception.TimeException; 18 | import com.hsn.epic4j.core.util.PageUtil; 19 | import com.ruiyun.jvppeteer.core.Constant; 20 | import com.ruiyun.jvppeteer.core.Puppeteer; 21 | import com.ruiyun.jvppeteer.core.browser.Browser; 22 | import com.ruiyun.jvppeteer.core.browser.BrowserFetcher; 23 | import com.ruiyun.jvppeteer.core.page.Page; 24 | import com.ruiyun.jvppeteer.options.LaunchOptions; 25 | import com.ruiyun.jvppeteer.options.LaunchOptionsBuilder; 26 | import com.ruiyun.jvppeteer.options.PageNavigateOptions; 27 | import com.ruiyun.jvppeteer.options.Viewport; 28 | import com.ruiyun.jvppeteer.util.FileUtil; 29 | import lombok.RequiredArgsConstructor; 30 | import lombok.SneakyThrows; 31 | import lombok.extern.slf4j.Slf4j; 32 | 33 | import java.util.*; 34 | import java.util.stream.Collectors; 35 | import java.util.stream.StreamSupport; 36 | 37 | /** 38 | * 主启动类 39 | * 40 | * @author hsn 41 | * 2021/12/27 42 | * EpicStart 43 | */ 44 | @Slf4j 45 | @RequiredArgsConstructor 46 | public class MainStart implements IStart { 47 | private final EpicConfig epicConfig; 48 | 49 | private final String LOADING_TEXT = "loading"; 50 | 51 | @Override 52 | @SneakyThrows 53 | public Browser getBrowser(String dataPath) { 54 | if (epicConfig.getNoSandbox()) { 55 | epicConfig.getDriverArgs().add("--no-sandbox"); 56 | } 57 | //自动下载,第一次下载后不会再下载 58 | if (Arrays.stream(Constant.EXECUTABLE_ENV).noneMatch(env -> { 59 | String chromeExecutable = System.getenv(env); 60 | return StrUtil.isNotBlank(chromeExecutable) && FileUtil.assertExecutable(chromeExecutable); 61 | })) { 62 | BrowserFetcher.downloadIfNotExist(epicConfig.getBrowserVersion()); 63 | } 64 | 65 | LaunchOptions options = new LaunchOptionsBuilder() 66 | .withArgs(epicConfig.getDriverArgs()) 67 | .withHeadless(epicConfig.getHeadLess()) 68 | .withUserDataDir(dataPath) 69 | .withIgnoreDefaultArgs(Collections.singletonList("--enable-automation")) 70 | .build(); 71 | return Puppeteer.launch(options); 72 | } 73 | 74 | @Override 75 | @Retry(message = "默认页面打开失败") 76 | public Page getDefaultPage(Browser browser) { 77 | List pages = browser.pages(); 78 | Page page = CollUtil.isNotEmpty(pages) ? pages.get(0) : browser.newPage(); 79 | Viewport viewport = new Viewport(); 80 | viewport.setWidth(600); 81 | viewport.setHeight(1000); 82 | viewport.setHasTouch(true); 83 | viewport.setIsMobile(true); 84 | page.setViewport(viewport); 85 | return page; 86 | } 87 | 88 | 89 | @SneakyThrows 90 | @Override 91 | @Retry(message = "登录检查失败") 92 | public boolean needLogin(Page page) { 93 | return page.$("div.mobile-buttons a[href='/login']") != null; 94 | // return PageUtil.getJsonValue(browser, epicConfig.getCheckLoginUrl(), "needLogin", Boolean.class); 95 | } 96 | 97 | 98 | private Boolean isInLibrary(Page page) { 99 | String textContent = PageUtil.getTextContent(page, "div[data-component=DesktopSticky] button[data-testid=purchase-cta-button]"); 100 | return "In Library".equals(textContent); 101 | } 102 | 103 | 104 | private String getItemUrl(Item item) { 105 | String url; 106 | if (Item.DLC.equals(item.getOfferType())) { 107 | url = item.getUrlSlug(); 108 | } else { 109 | url = item.getProductSlug(); 110 | 111 | } 112 | if (url != null) { 113 | return url; 114 | } 115 | //url为空尝试加载 offerMappings 116 | if (CollUtil.isNotEmpty(item.getOfferMappings())) { 117 | if ((url = findMappingUrl(item.getOfferMappings())) != null) { 118 | return url; 119 | } 120 | } 121 | //url为空尝试加载 catalogNs 122 | if (item.getCatalogNs() != null && CollUtil.isNotEmpty(item.getCatalogNs().getMappings())) { 123 | if ((url = findMappingUrl(item.getCatalogNs().getMappings())) != null) { 124 | return url; 125 | } 126 | } 127 | return null; 128 | } 129 | 130 | private String findMappingUrl(List pageSlugs) { 131 | return pageSlugs.stream().filter(i -> "productHome".equals(i.getPageType())) 132 | .findFirst().map(PageSlug::getPageSlug).orElse(null); 133 | } 134 | 135 | @Override 136 | @SneakyThrows 137 | @Retry(message = "领取失败") 138 | public List receive(Page page, List weekFreeItems) { 139 | if (log.isDebugEnabled()) { 140 | log.debug("所有免费的游戏:{}", weekFreeItems.stream().map(Item::getTitle).collect(Collectors.joining(","))); 141 | } 142 | List receiveItem = new ArrayList<>(); 143 | for (Item item : weekFreeItems) { 144 | String url = getItemUrl(item); 145 | String itemUrl = StrUtil.format(UrlConstants.storeUrl, url); 146 | log.info("游戏url:{}", itemUrl); 147 | page.goTo(itemUrl); 148 | log.info("18+检测"); 149 | PageUtil.tryClick(page, itemUrl, 8, 1000, "div[data-component=PDPAgeGate] Button"); 150 | PageUtil.waitForTextChange(page, "div[data-component=DesktopSticky] button[data-testid=purchase-cta-button]", LOADING_TEXT); 151 | if (isInLibrary(page)) { 152 | log.debug("游戏[{}]已经在库里", item.getTitle()); 153 | continue; 154 | } 155 | page.waitForSelector("div[data-component=DesktopSticky] button[data-testid=purchase-cta-button]").click(); 156 | // page.waitForSelector("div[data-component=WithClickTracking] button").click(); 157 | //epic user licence check 158 | log.info("首次领取游戏检测||设备检测"); 159 | PageUtil.tryClick(page, itemUrl, 30, 100, Arrays.asList( 160 | userLicenceCheck(), 161 | platformCheck() 162 | )); 163 | String purchaseUrl = PageUtil.getStrProperty(page, "#webPurchaseContainer iframe", "src"); 164 | log.debug("订单链接 :{}", purchaseUrl); 165 | page.goTo(purchaseUrl); 166 | PageUtil.tryClick(page, page.mainFrame().url(), 20, 500, "#purchase-app button[class*=confirm]:not([disabled])"); 167 | PageUtil.tryClick(page, page.mainFrame().url(), "#purchaseAppContainer div.payment-overlay button.payment-btn--primary"); 168 | PageUtil.findSelectors(page, 30_000, true, 169 | () -> { 170 | throw new TimeException("订单状态检测超时"); 171 | }, 172 | new SelectItem("#purchase-app div[class*=alert]", () -> { 173 | if (item.isDLC()) { 174 | //DLC情况下,在没有本体的情况下也也可以领取 175 | return SelectItem.SelectCallBack.CONTINUE; 176 | } else { 177 | String message = PageUtil.getStrProperty(page, "#purchase-app div[class*=alert]:not([disabled])", "textContent"); 178 | throw new PermissionException(message); 179 | } 180 | }), 181 | //#talon_container_checkout_free_prod 182 | //talon_frame_checkout_free_prod 183 | new SelectItem("#talon_container_checkout_free_prod[style*=visible]", () -> { 184 | //需要验证码 185 | throw new PermissionException("检测到需要验证码"); 186 | }), 187 | new SelectItem("#purchase-app > div", (p, i) -> p.$(i.getSelectors()) == null, () -> { 188 | //当订单完成刷新时,该元素不存在,是订单完成后刷新到新页面 189 | page.goTo(itemUrl); 190 | PageUtil.waitForTextChange(page, "div[data-component=DesktopSticky] button[data-testid=purchase-cta-button]", LOADING_TEXT); 191 | if (!isInLibrary(page)) { 192 | throw new ItemException("该游戏被误认为已经认领"); 193 | } 194 | log.info("游戏领取成功:{}", item.getTitle()); 195 | receiveItem.add(item); 196 | return SelectItem.SelectCallBack.END; 197 | }) 198 | ); 199 | } 200 | return receiveItem; 201 | } 202 | 203 | private PageUtil.EBiConsumer platformCheck() { 204 | return (p, c) -> { 205 | log.trace("平台不支持检测:{}", c); 206 | p.click("div[data-component=WarningLayout] button[data-component=BaseButton]"); 207 | log.info("平台不支持检测测通过"); 208 | }; 209 | } 210 | 211 | private PageUtil.EBiConsumer userLicenceCheck() { 212 | return (p, c) -> { 213 | log.trace("首次领取游戏检测"); 214 | p.click("#agree"); 215 | p.click("div[data-component=EulaModalActions] button[data-component=BaseButton]"); 216 | log.info("首次领取游戏检测通过"); 217 | }; 218 | } 219 | 220 | /** 221 | * 获取免费游戏 222 | */ 223 | @Override 224 | @Retry(message = "获取周末游戏失败", value = 10) 225 | public List getFreeItems() { 226 | //https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=zh-CN-CN&country=CN&allowCountries=CN 227 | String userCountry = "CN"; 228 | String locate = "zh-CN"; 229 | String formatUrl = StrUtil.format(UrlConstants.freeGameUrl, locate, userCountry, userCountry); 230 | log.debug(formatUrl); 231 | String res = HttpUtil.get(formatUrl); 232 | log.trace("免费游戏json串:\n{}", res); 233 | JSONObject json = JSONUtil.parseObj(res); 234 | List list = new ArrayList<>(); 235 | DateTime now = DateUtil.date(); 236 | for (JSONObject element : json.getByPath("data.Catalog.searchStore.elements", JSONArray.class).jsonIter()) { 237 | if (!"ACTIVE".equals(element.getStr("status"))) { 238 | continue; 239 | } 240 | if (StreamSupport.stream(element.getJSONArray("categories").jsonIter().spliterator(), false) 241 | .anyMatch(item -> "freegames".equals(item.getStr("path")))) { 242 | JSONObject promotions = element.getJSONObject("promotions"); 243 | if (promotions == null) { 244 | continue; 245 | } 246 | JSONArray promotionalOffers = promotions.getJSONArray("promotionalOffers"); 247 | if (CollUtil.isNotEmpty(promotionalOffers)) { 248 | if (StreamSupport.stream(promotionalOffers.jsonIter().spliterator(), false) 249 | .flatMap(offerItem -> StreamSupport.stream(offerItem.getJSONArray("promotionalOffers").jsonIter().spliterator(), false)) 250 | .anyMatch(offerItem -> { 251 | DateTime startDate = DateUtil.parse(offerItem.getStr("startDate")).setTimeZone(TimeZone.getDefault()); 252 | DateTime endDate = DateUtil.parse(offerItem.getStr("endDate")).setTimeZone(TimeZone.getDefault()); 253 | JSONObject discountSetting = offerItem.getJSONObject("discountSetting"); 254 | return DateUtil.isIn(now, startDate, endDate) && "PERCENTAGE".equals(discountSetting.getStr("discountType")) 255 | && discountSetting.getInt("discountPercentage") == 0; 256 | })) { 257 | list.add(element.toBean(Item.class)); 258 | } 259 | 260 | } 261 | } 262 | 263 | } 264 | return list; 265 | } 266 | 267 | /** 268 | * 跳转到epic 269 | */ 270 | @Override 271 | @SneakyThrows 272 | @Retry(message = "跳转epic", value = 5) 273 | public void goToEpic(Page page) { 274 | PageNavigateOptions options = new PageNavigateOptions(); 275 | options.setTimeout(ThreadContext.getTimeout()); 276 | page.goTo(UrlConstants.epicUrl, options, true); 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/MavenUpdate.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import cn.hutool.http.HttpUtil; 5 | import cn.hutool.json.JSONObject; 6 | import cn.hutool.json.JSONUtil; 7 | import com.hsn.epic4j.core.util.VersionCompare; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.io.File; 12 | 13 | /** 14 | * 通过阿里云的maven仓库更新,有一定的延迟 15 | * 16 | * @author hsn 17 | * 2022/1/4 18 | * MavenUpdate 19 | */ 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | public class MavenUpdate implements IUpdate { 23 | private final String currentVersion; 24 | 25 | /** 26 | * 有0-4小时的延迟 27 | */ 28 | @Override 29 | public void checkForUpdate() { 30 | String sonatypeSearch = "https://search.maven.org/solrsearch/select?q=g:io.github.huisunan%20AND%20a:epic4j&start=0&rows=20"; 31 | String downloadUrl = "http://search.maven.org/remotecontent?filepath=io/github/huisunan/epic4j/{}/epic4j-{}.jar"; 32 | String string = HttpUtil.get(sonatypeSearch); 33 | JSONObject jsonObject = JSONUtil.parseObj(string); 34 | Integer numFound = (Integer) jsonObject.getByPath("response.numFound"); 35 | if (numFound < 1) { 36 | log.warn("not found jar package"); 37 | return; 38 | } 39 | String lastVersion = (String) jsonObject.getByPath("response.docs[0].latestVersion"); 40 | 41 | VersionCompare compare = new VersionCompare(); 42 | if (compare.compare(lastVersion, currentVersion) > 0) { 43 | //需要更新 44 | String url = StrUtil.format(downloadUrl, lastVersion, lastVersion); 45 | File file = new File("epic4j.jar.update"); 46 | HttpUtil.downloadFile(url, file); 47 | log.info("download new version:{}", lastVersion); 48 | System.exit(UPDATE_EXIT_CODE); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/PasswordLogin.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.hsn.epic4j.core.bean.SelectItem; 5 | import com.hsn.epic4j.core.exception.CheckException; 6 | import com.hsn.epic4j.core.exception.PermissionException; 7 | import com.hsn.epic4j.core.exception.TimeException; 8 | import com.hsn.epic4j.core.util.PageUtil; 9 | import com.ruiyun.jvppeteer.core.page.Page; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | public class PasswordLogin implements ILogin { 17 | @Override 18 | @SneakyThrows 19 | public void login(Page page, String email, String password) { 20 | 21 | if (StrUtil.isEmpty(email)) { 22 | throw new CheckException("账号不能为空"); 23 | } 24 | if (StrUtil.isEmpty(password)) { 25 | throw new CheckException(email + " 密码不能为空"); 26 | } 27 | log.debug("开始登录"); 28 | String originUrl = page.mainFrame().url(); 29 | PageUtil.click(page, "div.menu-icon"); 30 | PageUtil.click(page, "div.mobile-buttons a[href='/login']"); 31 | PageUtil.waitUrlChange(page,originUrl); 32 | PageUtil.click(page, "#login-with-epic"); 33 | PageUtil.type(page, "#email", email); 34 | PageUtil.type(page, "#password", password); 35 | PageUtil.click(page, "#sign-in[tabindex='0']"); 36 | PageUtil.findSelectors(page, 30000, true, 37 | () -> { 38 | throw new TimeException("登录超时"); 39 | }, 40 | new SelectItem("#talon_frame_login_prod[style*=visible]", () -> { 41 | throw new PermissionException("未知情况下需要验证码"); 42 | }), 43 | new SelectItem("div.MuiPaper-root[role=alert] h6[class*=subtitle1]", () -> { 44 | Object jsonValue = page.waitForSelector("div.MuiPaper-root[role=alert] h6[class*=subtitle1]").getProperty("textContent").jsonValue(); 45 | throw new PermissionException("来自epic的错误消息: " + jsonValue); 46 | }), 47 | new SelectItem("input[name=code-input-0]", () -> { 48 | throw new PermissionException("需要校验码"); 49 | }), 50 | new SelectItem(".signed-in", () -> { 51 | log.info("登录成功"); 52 | return SelectItem.SelectCallBack.END; 53 | }) 54 | ); 55 | 56 | log.debug("登录结束"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/Retry.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(value = ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Retry { 11 | int value() default 3; 12 | 13 | String message() default "重试失败"; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/StartProxy.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.lang.reflect.InvocationHandler; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | @Slf4j 12 | @RequiredArgsConstructor 13 | public class StartProxy implements InvocationHandler { 14 | 15 | private final IStart target; 16 | 17 | @Override 18 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 19 | Retry annotation = target.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes()).getAnnotation(Retry.class); 20 | if (annotation == null) { 21 | return method.invoke(target, args); 22 | } 23 | int retryCount = annotation.value(); 24 | for (int i = 0; i < retryCount; i++) { 25 | try { 26 | return method.invoke(target, args); 27 | } catch (Throwable throwable) { 28 | Throwable targetThrowable; 29 | if (throwable instanceof InvocationTargetException){ 30 | InvocationTargetException invocationTargetException = (InvocationTargetException) throwable; 31 | targetThrowable = invocationTargetException.getTargetException(); 32 | }else { 33 | targetThrowable = throwable; 34 | } 35 | if (i == (retryCount - 1)) { 36 | log.error(annotation.message(), targetThrowable); 37 | throw throwable; 38 | } 39 | log.error("重试异常信息:{}", targetThrowable.getMessage()); 40 | log.debug("重试信息:{}, {} 第{}次执行", annotation.message(), method.getName(), i+1); 41 | } 42 | } 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/ThreadContext.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class ThreadContext { 7 | private final ThreadLocal timeout = new ThreadLocal<>(); 8 | private final ThreadLocal interval = new ThreadLocal<>(); 9 | 10 | public Integer getTimeout() { 11 | return timeout.get(); 12 | } 13 | 14 | public Integer getInterval() { 15 | return interval.get(); 16 | } 17 | 18 | public void init(Integer t,Integer i){ 19 | timeout.set(t); 20 | interval.set(i); 21 | } 22 | 23 | public void clear(){ 24 | timeout.remove(); 25 | interval.remove(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/UrlConstants.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | public interface UrlConstants { 4 | /** 5 | * 是否登录判断url 6 | */ 7 | String checkLoginUrl = "https://www.epicgames.com/account/v2/ajaxCheckLogin"; 8 | /** 9 | * 获取用户信息url 10 | */ 11 | String userInfoUrl = "https://www.epicgames.com/account/v2/personal/ajaxGet?sessionInvalidated=true"; 12 | /** 13 | * 免费游戏url 14 | */ 15 | String freeGameUrl = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale={}&country={}&allowCountries={}"; 16 | /** 17 | * 商店项url 18 | */ 19 | String storeUrl = "https://store.epicgames.com/en-US/p/{}"; 20 | /** 21 | * epic主页 22 | */ 23 | String epicUrl = "https://www.epicgames.com/store/en-US/"; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/WatchDogThread.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core; 2 | 3 | import com.hsn.epic4j.core.util.ScreenShootUtil; 4 | import com.ruiyun.jvppeteer.core.browser.Browser; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | @Slf4j 10 | public class WatchDogThread extends Thread { 11 | private final Browser browser; 12 | 13 | private final Thread workThread; 14 | 15 | private final Boolean screenShoot; 16 | 17 | public WatchDogThread(Browser browser, Thread workThread, Boolean screenShoot) { 18 | this.browser = browser; 19 | this.workThread = workThread; 20 | this.screenShoot = screenShoot; 21 | } 22 | 23 | @Override 24 | public void run() { 25 | while (true) { 26 | if (screenShoot) { 27 | ScreenShootUtil.screen(browser, "data/status", "status.jpeg"); 28 | } 29 | try { 30 | TimeUnit.SECONDS.sleep(1); 31 | } catch (InterruptedException e) { 32 | break; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/AliMvnDto.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author hsn 9 | * 2022/1/4 10 | * AliMvnDto 11 | */ 12 | @Data 13 | public class AliMvnDto { 14 | private List object; 15 | private Boolean successful; 16 | 17 | @Data 18 | public static class AliMvnItemDto { 19 | private String artifactId; 20 | private String classifier; 21 | private String fileName; 22 | private String groupId; 23 | private String id; 24 | private String packaging; 25 | private String repositoryId; 26 | private String version; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/CatalogNs.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class CatalogNs { 9 | private List mappings; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/Item.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | @Data 9 | @Builder 10 | public class Item { 11 | //基础游戏 12 | public static String BASE_GAME = "BASE_GAME"; 13 | public static String DLC = "DLC"; 14 | public static String OTHERS ="OTHERS"; 15 | 16 | 17 | private String title; 18 | private String offerId; 19 | private String namespace; 20 | private String offerType; 21 | private String productSlug; 22 | private String urlSlug; 23 | 24 | private CatalogNs catalogNs; 25 | private List offerMappings; 26 | 27 | public boolean isDLC() { 28 | return DLC.equals(offerType); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/PageSlug.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class PageSlug { 7 | private String pageSlug; 8 | private String pageType; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/SelectItem.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import com.ruiyun.jvppeteer.core.page.Page; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author hsn 10 | * 2022/1/14 11 | * SelectItem 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class SelectItem { 17 | private String selectors; 18 | private SelectPredicate pagePredicate; 19 | private SelectCallBack callback; 20 | 21 | public SelectItem(String selectors, SelectCallBack callback) { 22 | this(selectors, (page, item) -> page.$(item.getSelectors()) != null, callback); 23 | } 24 | 25 | 26 | public interface SelectCallBack { 27 | boolean CONTINUE = false; 28 | boolean END = true; 29 | 30 | /** 31 | * @return 是否终端运行 true终端 32 | * @throws RuntimeException RuntimeException 33 | * @throws InterruptedException InterruptedException 34 | */ 35 | boolean run() throws RuntimeException, InterruptedException; 36 | } 37 | 38 | public interface SelectPredicate { 39 | boolean test(P1 page, P2 selectItem); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/bean/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.bean; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @author hsn 9 | * 2022/1/6 10 | * UserInfo 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class UserInfo { 16 | /** 17 | * 邮箱 18 | */ 19 | private String email; 20 | /** 21 | * 密码 22 | */ 23 | private String password; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/config/EpicConfig.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.config; 2 | 3 | import com.hsn.epic4j.core.bean.UserInfo; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author hsn 11 | * 2021/12/27 12 | * EpicConig 13 | */ 14 | @EqualsAndHashCode(callSuper = true) 15 | @Data 16 | public class EpicConfig extends UserInfo { 17 | /** 18 | * webDriver的用户数据 19 | */ 20 | private String dataPath = "./data/chrome"; 21 | /** 22 | * webDriver启动参数 23 | */ 24 | private List driverArgs; 25 | /** 26 | * 无头模式 27 | */ 28 | private Boolean headLess = true; 29 | /** 30 | * 浏览器版本 31 | */ 32 | private String browserVersion; 33 | 34 | /** 35 | * crontab 表达式 36 | */ 37 | private String cron; 38 | /** 39 | * 非沙盒运行 40 | */ 41 | private Boolean noSandbox = true; 42 | /** 43 | * cookiePath 44 | */ 45 | private String cookiePath; 46 | /** 47 | * 版本 48 | */ 49 | private String version; 50 | /** 51 | * 更新类型 52 | */ 53 | private String updateType; 54 | /** 55 | * 自动更新 56 | */ 57 | private Boolean autoUpdate; 58 | /** 59 | * 多用户模式 60 | */ 61 | private Boolean multiUser; 62 | /** 63 | * 多用户配置 64 | */ 65 | private List users; 66 | /** 67 | * 错误输出截图 68 | */ 69 | private Boolean errorScreenShoot = true; 70 | /** 71 | * 超时时间ms 72 | */ 73 | private Integer timeout = 30_000; 74 | /** 75 | * 间隔时间ms 76 | */ 77 | private Integer interval = 100; 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/exception/CheckException.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.exception; 2 | 3 | public class CheckException extends RuntimeException { 4 | public CheckException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/exception/ItemException.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.exception; 2 | 3 | 4 | /** 5 | * @author hsn 6 | * 2022/1/14 7 | * ItemException 8 | */ 9 | 10 | public class ItemException extends RuntimeException { 11 | 12 | public ItemException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/exception/OMSException.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.exception; 2 | 3 | public class OMSException extends RuntimeException { 4 | public OMSException() { 5 | super("need one more step"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/exception/PermissionException.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.exception; 2 | 3 | public class PermissionException extends RuntimeException{ 4 | public PermissionException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/exception/TimeException.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.exception; 2 | 3 | public class TimeException extends RuntimeException{ 4 | public TimeException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/notify/ConsoleNotify.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.notify; 2 | 3 | import com.hsn.epic4j.core.bean.Item; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | @Component 11 | @Slf4j 12 | public class ConsoleNotify implements INotify { 13 | @Override 14 | public boolean notifyReceive(List list) { 15 | log.info("所有领取到的游戏:{}",list.stream().map(Item::getTitle).collect(Collectors.joining(","))); 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/notify/INotify.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.notify; 2 | 3 | import com.hsn.epic4j.core.bean.Item; 4 | 5 | import java.util.List; 6 | 7 | public interface INotify { 8 | /** 9 | * 领取成功通知 10 | * @param list 成功的列表 11 | * @return 是否阻断 true阻断 12 | */ 13 | boolean notifyReceive(List list); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/util/PageUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.util; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import cn.hutool.json.JSONObject; 5 | import cn.hutool.json.JSONUtil; 6 | import com.hsn.epic4j.core.ThreadContext; 7 | import com.hsn.epic4j.core.bean.SelectItem; 8 | import com.hsn.epic4j.core.exception.TimeException; 9 | import com.ruiyun.jvppeteer.core.browser.Browser; 10 | import com.ruiyun.jvppeteer.core.page.ElementHandle; 11 | import com.ruiyun.jvppeteer.core.page.Page; 12 | import com.ruiyun.jvppeteer.core.page.Response; 13 | import com.ruiyun.jvppeteer.protocol.PageEvaluateType; 14 | import lombok.SneakyThrows; 15 | import lombok.experimental.UtilityClass; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.util.Collections; 19 | import java.util.HashSet; 20 | import java.util.List; 21 | import java.util.Set; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.concurrent.TimeoutException; 24 | import java.util.concurrent.atomic.AtomicReference; 25 | 26 | @Slf4j 27 | @UtilityClass 28 | public class PageUtil { 29 | @SneakyThrows 30 | public JSONObject getJson(String url, Browser browser) { 31 | Page page = browser.newPage(); 32 | Response response = page.goTo(url); 33 | String text = response.text(); 34 | log.trace("get {} json value : {}", url, text); 35 | JSONObject jsonObject = JSONUtil.parseObj(text); 36 | page.close(); 37 | return jsonObject; 38 | } 39 | 40 | 41 | /** 42 | * 在指定范围时间内,遍历查找 43 | * 44 | * @param page page 45 | * @param timeout 超时时间 46 | * @param ignore true忽略异常 47 | * @param timeoutBack 超时后回调 48 | * @param selectItems 要查询的元素 49 | */ 50 | @SneakyThrows 51 | public void findSelectors(Page page, Integer timeout, Boolean ignore, SelectItem.SelectCallBack timeoutBack, SelectItem... selectItems) { 52 | timer(timeout, ThreadContext.getInterval(), i -> { 53 | for (SelectItem selectItem : selectItems) { 54 | boolean flag = false; 55 | try { 56 | flag = selectItem.getPagePredicate().test(page, selectItem); 57 | } catch (Exception e) { 58 | if (ignore) 59 | log.debug("可以忽略到异常", e); 60 | else 61 | throw e; 62 | } 63 | if (flag) { 64 | return selectItem.getCallback().run(); 65 | } 66 | } 67 | return false; 68 | }, timeoutBack::run); 69 | 70 | 71 | } 72 | 73 | 74 | public void waitForTextChange(Page page, String selector, String text) { 75 | waitForTextChange(page, selector, text, ThreadContext.getTimeout(), ThreadContext.getInterval()); 76 | } 77 | 78 | @SneakyThrows 79 | public void waitForTextChange(Page page, String selector, String text, Integer timeout, Integer interval) { 80 | timer(timeout, interval, i -> { 81 | ElementHandle elementHandle = page.$(selector); 82 | log.trace("wait {} text change count {}", selector, i); 83 | if (elementHandle != null) { 84 | String textContent = getElementStrProperty(elementHandle, "textContent"); 85 | log.trace("textContent:{}",textContent); 86 | return !text.equalsIgnoreCase(textContent); 87 | } 88 | return false; 89 | }, () -> { 90 | throw new TimeException("wait text change timeout :" + text); 91 | }); 92 | } 93 | 94 | 95 | public String getElementStrProperty(ElementHandle elementHandle, String property) { 96 | return (String) elementHandle.getProperty(property).jsonValue(); 97 | } 98 | 99 | public String getTextContent(Page page, String selector) { 100 | return getStrProperty(page, selector, "textContent"); 101 | } 102 | 103 | @SneakyThrows 104 | public String getStrProperty(Page page, String selector, String property) { 105 | AtomicReference res = new AtomicReference<>(); 106 | elementHandle(page, selector, ThreadContext.getTimeout(), ThreadContext.getInterval(), 107 | e -> res.set(getElementStrProperty(e, property))); 108 | return res.get(); 109 | } 110 | 111 | public void click(Page page, String selector) { 112 | elementHandle(page, selector, null, null, ElementHandle::click); 113 | } 114 | 115 | public void type(Page page, String selector, String type) { 116 | elementHandle(page, selector, null, null, c -> c.type(type)); 117 | } 118 | 119 | @SneakyThrows 120 | private void timer(Integer timeout, Integer interval, EFunction function, ERun end) { 121 | timeout = ObjectUtil.defaultIfNull(timeout, ThreadContext.getTimeout()); 122 | interval = ObjectUtil.defaultIfNull(interval, ThreadContext.getInterval()); 123 | for (int i = 0; (i * interval) < timeout; i++) { 124 | if (function.accept(i)) { 125 | return; 126 | } 127 | TimeUnit.MILLISECONDS.sleep(interval); 128 | } 129 | end.run(); 130 | 131 | } 132 | 133 | @SneakyThrows 134 | public void elementHandle(Page page, String selector, Integer timeout, Integer interval, EConsumer consumer) { 135 | timer(timeout, interval, i -> { 136 | log.trace("[{}]wait for selector:{}", i, selector); 137 | ElementHandle elementHandle = page.$(selector); 138 | if (elementHandle != null) { 139 | log.trace("start consumer"); 140 | consumer.accept(elementHandle); 141 | log.trace("end consumer"); 142 | //点击后延迟2秒等待处理 143 | TimeUnit.SECONDS.sleep(2); 144 | return true; 145 | } 146 | return false; 147 | }, () -> { 148 | throw new TimeoutException("wait for selector " + selector + " time out"); 149 | }); 150 | 151 | } 152 | 153 | public void tryClick(Page page, String original, String selector) { 154 | tryClick(page, original, 3, 500, selector); 155 | } 156 | 157 | public void tryClick(Page page, String original, Integer retry, Integer interval, String selector) { 158 | tryClick(page, original, retry, interval, Collections.singletonList((p, c) -> { 159 | log.trace("try click {} count:{}", selector, c); 160 | page.click(selector); 161 | })); 162 | } 163 | 164 | @SneakyThrows 165 | public void tryClick(Page page, String original, Integer retry, Integer interval, List> consumers) { 166 | Set> successSet = new HashSet<>(); 167 | for (int i = 0; i < retry; i++) { 168 | String mainFrameUrl = page.mainFrame().url(); 169 | log.trace("\nmainFrameUrl:{}\noriginal:{}",mainFrameUrl,original); 170 | if (!mainFrameUrl.equals(original)) { 171 | return; 172 | } 173 | for (EBiConsumer consumer : consumers) { 174 | try { 175 | consumer.accept(page, i); 176 | successSet.add(consumer); 177 | } catch (Exception ignored) { 178 | } 179 | TimeUnit.MILLISECONDS.sleep(interval); 180 | } 181 | if (successSet.size() >= consumers.size()) { 182 | return; 183 | } 184 | } 185 | } 186 | 187 | public void waitUrlChange(Page page, String originUrl) { 188 | timer(ThreadContext.getTimeout(), ThreadContext.getInterval(), i -> !originUrl.equals(page.mainFrame().url()), () -> { 189 | throw new TimeoutException("login url not change"); 190 | }); 191 | } 192 | 193 | public interface EConsumer { 194 | void accept(T t) throws Exception; 195 | } 196 | 197 | public interface EBiConsumer { 198 | void accept(T t, U u) throws Exception; 199 | } 200 | 201 | public interface ESupply { 202 | T get() throws Exception; 203 | } 204 | 205 | public interface EFunction { 206 | R accept(T t) throws Exception; 207 | } 208 | 209 | public interface ERun { 210 | void run() throws Exception; 211 | } 212 | 213 | public void crawSet(Page page) { 214 | String userAgent = (String) page.evaluate("navigator.userAgent"); 215 | page.evaluateOnNewDocument("() => {Object.defineProperty(window,'navigator',{value:new Proxy(navigator,{has:(target,key)=>(key==='webdriver'?false:key in target),get:(target,key)=>key==='webdriver'?false:typeof target[key]==='function'?target[key].bind(target):target[key]})})}", PageEvaluateType.FUNCTION); 216 | page.evaluateOnNewDocument("() => {Object.defineProperty(navigator, 'languages', {get: () => ['en','en-US']})}", PageEvaluateType.FUNCTION); 217 | // page.evaluateOnNewDocument("() => {Object.defineProperty(navigator, 'plugins', {get: () => [{'description': 'Portable Document Format', 'filename': 'internal-pdf-viewer', 'length': 1, 'name': 'Chrome PDF Plugin'}, {'description': '', 'filename': 'mhjfbmdgcfjbbpaeojofohoefgiehjai', 'length': 1, 'name': 'Chromium PDF Viewer'}, {'description': '', 'filename': 'internal-nacl-plugin', 'length': 2, 'name': 'Native Client'}]})}", PageEvaluateType.FUNCTION); 218 | page.evaluateOnNewDocument("() => {Reflect.defineProperty(navigator, 'mimeTypes', {get: () => [{type: 'application/pdf', suffixes: 'pdf', description: '', enabledPlugin: Plugin}, {type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: Plugin}, {type: 'application/x-nacl', suffixes: '', description: 'Native Client Executable', enabledPlugin: Plugin}, {type: 'application/x-pnacl', suffixes: '', description: 'Portable Native Client Executable', enabledPlugin: Plugin}]})}", PageEvaluateType.FUNCTION); 219 | page.evaluateOnNewDocument("window.chrome = {'loadTimes': {}, 'csi': ()=>{}, 'app': {'isInstalled': false, 'getDetails': {}, 'getIsInstalled': {}, 'installState': {}, 'runningState': {}, 'InstallState': {'DISABLED': 'disabled', 'INSTALLED': 'installed', 'NOT_INSTALLED': 'not_installed'}, 'RunningState': {'CANNOT_RUN': 'cannot_run', 'READY_TO_RUN': 'ready_to_run', 'RUNNING': 'running'}}, 'webstore': {'onDownloadProgress': {'addListener': {}, 'removeListener': {}, 'hasListener': {}, 'hasListeners': {}, 'dispatch': {}}, 'onInstallStageChanged': {'addListener': {}, 'removeListener': {}, 'hasListener': {}, 'hasListeners': {}, 'dispatch': {}}, 'install': {}, 'ErrorCode': {'ABORTED': 'aborted', 'BLACKLISTED': 'blacklisted', 'BLOCKED_BY_POLICY': 'blockedByPolicy', 'ICON_ERROR': 'iconError', 'INSTALL_IN_PROGRESS': 'installInProgress', 'INVALID_ID': 'invalidId', 'INVALID_MANIFEST': 'invalidManifest', 'INVALID_WEBSTORE_RESPONSE': 'invalidWebstoreResponse', 'LAUNCH_FEATURE_DISABLED': 'launchFeatureDisabled', 'LAUNCH_IN_PROGRESS': 'launchInProgress', 'LAUNCH_UNSUPPORTED_EXTENSION_TYPE': 'launchUnsupportedExtensionType', 'MISSING_DEPENDENCIES': 'missingDependencies', 'NOT_PERMITTED': 'notPermitted', 'OTHER_ERROR': 'otherError', 'REQUIREMENT_VIOLATIONS': 'requirementViolations', 'USER_CANCELED': 'userCanceled', 'WEBSTORE_REQUEST_ERROR': 'webstoreRequestError'}, 'InstallStage': {'DOWNLOADING': 'downloading', 'INSTALLING': 'installing'}}}", PageEvaluateType.FUNCTION); 220 | page.evaluateOnNewDocument("() => {Reflect.defineProperty(navigator.connection,'rtt', {get: () => 200, enumerable: true})}", PageEvaluateType.FUNCTION); 221 | page.evaluateOnNewDocument("() => { const getParameter = WebGLRenderingContext.getParameter;WebGLRenderingContext.prototype.getParameter = function (parameter) {if (parameter === 37445) { return 'Intel Inc.'; }if (parameter === 37446) {return 'Intel(R) Iris(TM) Graphics 6100';}return getParameter(parameter);};}", PageEvaluateType.FUNCTION); 222 | page.evaluateOnNewDocument("() => {const newProto = navigator.__proto__; delete newProto.webdriver; navigator.__proto__ = newProto}", PageEvaluateType.FUNCTION); 223 | page.evaluateOnNewDocument("() => {const p = {'defaultRequest': null, 'receiver': null}; Reflect.defineProperty(navigator, 'presentation', {get: () => p})}", PageEvaluateType.FUNCTION); 224 | page.setExtraHTTPHeaders(Collections.singletonMap("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")); 225 | page.setUserAgent(userAgent.replace("Headless", "")); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/util/ScreenShootUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.util; 2 | 3 | import cn.hutool.core.collection.CollUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.ruiyun.jvppeteer.core.browser.Browser; 6 | import com.ruiyun.jvppeteer.options.ScreenshotOptions; 7 | import lombok.Cleanup; 8 | import lombok.experimental.UtilityClass; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.core.io.FileUrlResource; 11 | 12 | import java.io.File; 13 | import java.io.FileOutputStream; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.Optional; 17 | 18 | @Slf4j 19 | @UtilityClass 20 | public class ScreenShootUtil { 21 | public void screen(Browser browser, String path, String fileName, String htmlName) { 22 | Optional.ofNullable(browser) 23 | .map(Browser::pages) 24 | .filter(CollUtil::isNotEmpty) 25 | .map(pages -> pages.get(0)) 26 | .ifPresent(page -> { 27 | try { 28 | FileUrlResource errorDir = new FileUrlResource(path); 29 | if (errorDir.getFile().mkdirs()) { 30 | log.debug("创建目录:{}", path); 31 | } 32 | ScreenshotOptions options = new ScreenshotOptions(); 33 | options.setQuality(100); 34 | String absolutePath = errorDir.getFile().getAbsolutePath() + File.separator; 35 | options.setPath(absolutePath + fileName); 36 | options.setType("jpeg"); 37 | page.screenshot(options); 38 | if (StrUtil.isNotBlank(htmlName)) { 39 | @Cleanup FileOutputStream fileOutputStream = new FileOutputStream(absolutePath + htmlName); 40 | fileOutputStream.write(page.content().getBytes(StandardCharsets.UTF_8)); 41 | } 42 | } catch (IOException ioException) { 43 | log.error("截图失败"); 44 | } 45 | }); 46 | } 47 | 48 | public void screen(Browser browser, String path, String fileName) { 49 | screen(browser, path, fileName, null); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/util/SelectorUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.util; 2 | 3 | import com.ruiyun.jvppeteer.options.WaitForSelectorOptions; 4 | import lombok.experimental.UtilityClass; 5 | 6 | @UtilityClass 7 | public class SelectorUtil { 8 | public WaitForSelectorOptions timeout(Integer timeout){ 9 | WaitForSelectorOptions options = new WaitForSelectorOptions(); 10 | options.setTimeout(timeout); 11 | return options; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsn/epic4j/core/util/VersionCompare.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j.core.util; 2 | 3 | import java.util.Comparator; 4 | 5 | /** 6 | * @author hsn 7 | * 2022/1/4 8 | * VersionCompare 9 | */ 10 | public class VersionCompare implements Comparator { 11 | 12 | @Override 13 | public int compare(String o1, String o2) { 14 | String[] split1 = o1.split("\\."); 15 | String[] split2 = o2.split("\\."); 16 | for (int i = 0; i < Math.max(split1.length, split2.length); i++) { 17 | //循环比较值 18 | Integer i1 = i < split1.length ? Integer.parseInt(split1[i]) : 0; 19 | Integer i2 = i < split2.length ? Integer.parseInt(split2[i]) : 0; 20 | int res = i1.compareTo(i2); 21 | if (res != 0) { 22 | return res; 23 | } 24 | } 25 | return 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | epic: 2 | driver-args: 3 | #- "--blink-settings=imagesEnabled=false" 4 | - "--no-first-run" 5 | - "--disable-gpu" 6 | - "--no-default-browser-check" 7 | email: ${EMAIL} 8 | password: ${PASSWORD} 9 | cookie-path: ${COOKIE_PATH} 10 | cron: ${CRON:} 11 | version: @project.version@ 12 | # 更新类型 13 | update-type: maven 14 | auto-update: ${AUTO_UPDATE:false} 15 | multi-user: false 16 | 17 | logging: 18 | level: 19 | com.hsn.epic4j: ${LOG_LEVEL:info} 20 | spring: 21 | messages: 22 | basename: i18n/message 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ______ _ _ _ _ 3 | | ____| (_) | || | (_) 4 | | |__ _ __ _ ___| || |_ _ 5 | | __| | '_ \| |/ __|__ _| | 6 | | |____| |_) | | (__ | | | | 7 | |______| .__/|_|\___| |_| | | 8 | | | _/ | 9 | |_| |__/ 10 | -------------------------------------------------------------------------------- /src/test/java/com/hsn/epic4j/CrawTest.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import com.ruiyun.jvppeteer.core.Puppeteer; 5 | import com.ruiyun.jvppeteer.core.browser.Browser; 6 | import com.ruiyun.jvppeteer.core.browser.BrowserFetcher; 7 | import com.ruiyun.jvppeteer.core.page.Page; 8 | import com.ruiyun.jvppeteer.options.LaunchOptions; 9 | import com.ruiyun.jvppeteer.options.LaunchOptionsBuilder; 10 | import com.ruiyun.jvppeteer.options.ScreenshotOptions; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.core.io.FileUrlResource; 14 | 15 | import java.io.File; 16 | import java.util.Arrays; 17 | import java.util.Collections; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | /** 21 | * @author hsn 22 | * 2022/1/5 23 | * CrawTest 24 | */ 25 | @Slf4j 26 | public class CrawTest { 27 | 28 | @SneakyThrows 29 | public void test() { 30 | 31 | BrowserFetcher.downloadIfNotExist("938248"); 32 | String dataPath = new FileUrlResource("./data/chrome/tests").getFile().getAbsolutePath(); 33 | LaunchOptions options = new LaunchOptionsBuilder() 34 | .withArgs(Arrays.asList("--blink-settings=imagesEnabled=false", "--no-first-run", "--disable-gpu", "--no-default-browser-check", "--no-sandbox")) 35 | .withHeadless(false) 36 | .withUserDataDir(dataPath) 37 | .withExecutablePath("") 38 | .withIgnoreDefaultArgs(Collections.singletonList("--enable-automation")) 39 | .build(); 40 | Browser browser = Puppeteer.launch(options); 41 | Page page = browser.pages().get(0); 42 | // PageUtil.crawSet(page); 43 | // page.goTo("http://localhost:9999/"); 44 | page.goTo("http://www.baidu.com/"); 45 | TimeUnit.SECONDS.sleep(10); 46 | FileUrlResource errorDir = new FileUrlResource("data/test"); 47 | File file = errorDir.getFile(); 48 | log.debug("mkdir {}", file.mkdirs()); 49 | ScreenshotOptions screenshotOptions = new ScreenshotOptions(); 50 | screenshotOptions.setQuality(100); 51 | screenshotOptions.setPath(file.getAbsolutePath() + File.separator + "report.jpg"); 52 | screenshotOptions.setType("jpeg"); 53 | page.screenshot(screenshotOptions); 54 | FileUtil.writeUtf8String(page.content(), new File(file.getAbsolutePath() + File.separator + "report.html")); 55 | log.info("success"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/hsn/epic4j/Epic4jApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.hsn.epic4j; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | 5 | @SpringBootTest 6 | class Epic4jApplicationTests { 7 | 8 | void contextLoads() { 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function start() { 3 | cd /opt/epic4j || exit 4 | if [ -e "epic4j.jar.update" ];then 5 | rm -rf epic4j.jar 6 | mv epic4j.jar.update epic4j.jar 7 | echo "update success" 8 | fi 9 | java -jar epic4j.jar 10 | res=$? 11 | return $res 12 | } 13 | 14 | start 15 | status=$? 16 | while [ $status -eq 66 ] 17 | do 18 | start 19 | status=$? 20 | echo "update event" 21 | done --------------------------------------------------------------------------------