Repository: mokeyjay/Pixiv-daily-ranking-widget
Branch: master
Commit: 96d829517f31
Files: 55
Total size: 87.3 KB
Directory structure:
gitextract_29qs4ryc/
├── .docker/
│ ├── Dockerfile
│ ├── config.php
│ ├── crontab
│ ├── nginx-site.conf
│ └── start.sh
├── .github/
│ └── workflows/
│ ├── develop.yml
│ └── docker-image.yml
├── .gitignore
├── .test/
│ └── test_image_hosting.php
├── LICENSE
├── README.en.md
├── README.md
├── app/
│ ├── App.php
│ ├── Controllers/
│ │ ├── ApiController.php
│ │ ├── Controller.php
│ │ └── IndexController.php
│ ├── Factory.php
│ ├── ImageHosting/
│ │ ├── Catbox.php
│ │ ├── Chkaja.php
│ │ ├── FiftyEight.php
│ │ ├── ImageHosting.php
│ │ ├── Local.php
│ │ ├── Riyugo.php
│ │ ├── Smms.php
│ │ └── Tietuku.php
│ ├── Jobs/
│ │ ├── ClearLog.php
│ │ ├── Job.php
│ │ └── Refresh.php
│ ├── Libs/
│ │ ├── Config.php
│ │ ├── Curl.php
│ │ ├── Env.php
│ │ ├── Lock.php
│ │ ├── Log.php
│ │ ├── Pixiv.php
│ │ ├── Request.php
│ │ ├── Response.php
│ │ ├── Storage.php
│ │ └── Str.php
│ ├── Views/
│ │ ├── index.php
│ │ └── loading.php
│ └── autoload.php
├── config.php
├── demo.html
├── doc/
│ ├── advance-usage.en.md
│ ├── advance-usage.md
│ ├── deploy.en.md
│ ├── deploy.md
│ ├── docker.en.md
│ ├── docker.md
│ ├── log.en.md
│ └── log.md
├── index.php
└── storage/
├── app/
│ └── .gitignore
├── images/
│ └── .gitignore
└── logs/
└── .gitignore
================================================
FILE CONTENTS
================================================
================================================
FILE: .docker/Dockerfile
================================================
FROM ghcr.io/mokeyjay/pixiv-widget-image:main
COPY . /var/www/html
COPY .docker/config.php /var/www/html/config.php
COPY .docker/nginx-site.conf /etc/nginx/conf.d/default.conf
COPY .docker/start.sh /root/start.sh
COPY .docker/crontab /etc/cron.d/pixiv-cron-job
RUN chmod 0644 /etc/cron.d/pixiv-cron-job && \
crontab /etc/cron.d/pixiv-cron-job
ENV TZ=Asia/Shanghai
LABEL Author="mokeyjay"
LABEL Version="2023.11.20"
LABEL Description="Pixiv 每日排行榜小挂件"
CMD cron && \
chmod +x /root/start.sh && \
/root/start.sh
================================================
FILE: .docker/config.php
================================================
Env::getStr('URL'),
'background_color' => Env::getStr('BACKGROUND_COLOR', 'transparent'),
'limit' => Env::getStr('LIMIT', 50),
'service' => Env::getBool('SERVICE', true),
'log_level' => Env::getArray('LOG_LEVEL', ['DEBUG', 'ERROR']),
'proxy' => Env::getStr('PROXY'),
'clear_overdue' => Env::getBool('CLEAR_OVERDUE', true),
'compress' => Env::getBool('COMPRESS', true),
'image_hosting' => Env::getArray('IMAGE_HOSTING', ['local']),
'image_hosting_extend' => [
'tietuku' => [
'token' => Env::getStr('IMAGE_HOSTING_EXTEND_TIETUKU_TOKEN'),
],
'smms' => [
'token' => Env::getStr('IMAGE_HOSTING_EXTEND_SMMS_TOKEN'),
],
'riyugo' => [
'url' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_URL'),
'upload_path' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_UPLOAD_PATH'),
'unique_id' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_UNIQUE_ID'),
'token' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_TOKEN'),
],
],
'disable_web_job' => Env::getBool('DISABLE_WEB_JOB', true),
'header_script' => Env::getStr('HEADER_SCRIPT', ''),
'ranking_type' => Env::getStr('RANKING_TYPE', ''),
];
================================================
FILE: .docker/crontab
================================================
*/30 * * * * cd /var/www/html/ && /usr/local/bin/php index.php -j=refresh
30 1 * * * cd /var/www/html/ && /usr/local/bin/php index.php -j=clear-log
================================================
FILE: .docker/nginx-site.conf
================================================
server {
listen 80;
listen [::]:80;
server_name _;
root /var/www/html;
index index.php;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ /\. {
deny all;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
}
}
================================================
FILE: .docker/start.sh
================================================
#!/bin/bash
echo '' > /etc/nginx/sites-enabled/default
cd /var/www/html
# 将环境变量保存起来,免得 crontab 读不到
env=("URL" "BACKGROUND_COLOR" "LIMIT" "SERVICE" "LOG_LEVEL" "PROXY" "CLEAR_OVERDUE" "COMPRESS" "IMAGE_HOSTING" "IMAGE_HOSTING_EXTEND_TIETUKU_TOKEN" "IMAGE_HOSTING_EXTEND_SMMS_TOKEN" "IMAGE_HOSTING_EXTEND_RIYUGO_URL" "IMAGE_HOSTING_EXTEND_RIYUGO_UPLOAD_PATH" "IMAGE_HOSTING_EXTEND_RIYUGO_UNIQUE_ID" "IMAGE_HOSTING_EXTEND_RIYUGO_TOKEN" "DISABLE_WEB_JOB" "HEADER_SCRIPT" "RANKING_TYPE")
file=".env"
> $file
for var in "${env[@]}"
do
# 部分环境变量值含有空格、换行,得用双引号包起来,不然 source 时会报错
value="${!var}"
value="${value//\"/\\\"}"
echo "$var=\"$value\"" >> $file
done
chown -R www-data:www-data /var/www/html
php-fpm -D
nginx -g 'daemon off;'
================================================
FILE: .github/workflows/develop.yml
================================================
name: 推送开发用的 docker 镜像
on:
push:
branches:
- 'develop'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
push_to_registry:
name: 构建并推送
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# https://github.com/actions/checkout
- name: 拉取代码
uses: actions/checkout@v4
with:
ref: develop
# https://github.com/docker/login-action
- name: 登录到 ghcr
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/docker/metadata-action
- name: 提取事件元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# https://github.com/docker/setup-buildx-action
- name: 使用 buildx 作为构建器
uses: docker/setup-buildx-action@v3
# https://github.com/docker/build-push-action
- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
file: .docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/docker-image.yml
================================================
# action 的名称
name: 推送 Docker 镜像
# 触发 action 的事件
on:
push:
# master 分支有推送时触发
branches:
- 'master'
# tag 新建时触发
tags:
- '*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
push_to_registry:
name: 构建并推送
# 基于指定平台构建。有 win、ubuntu、mac 可选
# 消耗的分钟倍数:linux 1x、win 2x、mac 10x
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# https://github.com/actions/checkout
- name: 拉取代码
uses: actions/checkout@v4
# https://github.com/docker/login-action
- name: 登录到 ghcr
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 从触发此次 action 的事件中提取源数据(tag、label 什么的)
# https://github.com/docker/metadata-action
- name: 提取事件元数据
id: meta
uses: docker/metadata-action@v5
with:
# 提取出来的源数据用于这个镜像
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# 使用 buildx 作为构建器,以支持多平台构建之类的能力
# https://github.com/docker/setup-buildx-action
- name: 使用 buildx 作为构建器
uses: docker/setup-buildx-action@v3
# https://github.com/docker/build-push-action
- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
file: .docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .gitignore
================================================
/.idea
.DS_Store
================================================
FILE: .test/test_image_hosting.php
================================================
upload(TEST_PATH . 'pic.jpg')) {
var_dump($className . '成功: ' . $url);
} else {
var_dump($className . '失败: ' . $url);
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 mokeyjay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.en.md
================================================
🖼️ Pixiv Daily Ranking Widget
中文
Want to add a Pixiv Daily Ranking Widget to your website? It's a matter of one line of code!
DEMO
## ✨ Features
- Easy to use with one line of `HTML` code
- Adaptive width and height. A minimum size of `240px * 380px` (width \* height) or more is recommanded
- Redirect to artwork page by clicking on the widget
- Automatic daily update
- Low system resource utilisation with supports for multiple image hosting platforms and on-demand image loading
- Offering an API that includes ranking update date, thumbnail URL and detail page URL etc.
## 🤔 How to use
Just add the below code to your page
```html
```
Taking `WordPress` as an example. On `wp-admin`, click on **Appearance** -> **Widgets**
Then add a **Text** or **Custom HTML** widget as deemed appropriate on the right and fill the code above in
[Advance Usage](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/advance-usage.en.md)
## 🛠️ How to deploy
Wanted to customize the code yourself? Thought the service I provided is slow in speed?
You can also easily deploy your own widget!
> Requires Docker or PHP version >= 5.6
[Deployment Documentation](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.en.md)
## 🔌 APIs
[Ranking data (images hosted privately)](https://pixiv.mokeyjay.com/?r=api/pixiv-json) (recommended)
[Ranking data (pixiv url)](https://pixiv.mokeyjay.com/?r=api/source-json)
In which `data` is the data of the ranking table; `date` is the date of ranking (could be yesterday or the day before, as the time of refresh on Pixiv is not certain)
Both APIs automatically return the respective cross-domain header according to `Origin` or `Referer` within the request header. The APIs are front-end ready.
> The `image` and `url` keys are for compatibility purposes for users of 4.x or earlier versions, they can be ignored
## 🆙 Upgrading Guide
### Upgrading From 5.2 to 6.0
1. [Download the Source Code](https://github.com/mokeyjay/pixiv-daily-ranking-widget/releases/latest)
2. Unzip and overwrite the `app` and `index.php` to your server
> **⚠️ For Docker User**
> - Please replace all `-` in the environment variable name with `_`
> - Docker Image migration from Docker Hub to [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)
## 🌟 Changelog
### New Features
- Added `ranking_type` configuration option, which now allows you to select whether to fetch the overall or illustration/manga daily rankings.
- Added image preloading to improve the experience in poor network environments.
### Optimizations
- Completely rewritten the frontend with more elegant animation effects.
- Removed the dependency on Bootstrap for faster loading.
- Switched to using the official PHP and Nginx packages.
- No longer repeatedly check integrity when retrieving images from local storage.
- The `background_color` configuration now supports [more colors](https://developer.mozilla.org/en-US/docs/Web/CSS/background-color)
### Fixes
- Some environment variables cannot be obtained normally in some cases
- The scheduled task actually runs once every hour, not every half hour as stated in the documentation
### Other
- Removed the `static_cdn` configuration option due to the removal of the dependency on Bootstrap.
- Removed the invalid `Pngcm` and `Tsesze` image-hosting
- Docker image migration to [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)
- Docker image timezone defaults to Shanghai
[History](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/log.en.md)
## 👨💻 About author
[mokeyjay](https://www.mokeyjay.com), IT and ACG lover
================================================
FILE: README.md
================================================
🖼️ Pixiv 每日排行榜小挂件
English
想要在你的网站页面中添加一个 Pixiv 每日排行榜 的展示功能吗?现在,只需要一行代码即可实现!
在线预览
## ✨ 特色
- 一行 `HTML` 代码即可调用,方便快捷
- 自适应宽高。推荐宽度 `240px`、高度 `380px` 或以上
- 点击图片可跳转到对应作品详情页
- 每日自动更新,无需人工干预
- 内置多图床支持、按需加载图片,极低资源消耗
- 提供 API 服务,含有排行榜更新日期、缩略图 url 及详情页 url 等
## 🤔 如何使用
将这行代码添加到网页上即可
```html
```
以 `Wordpress` 为例。首先进入后台,点击 外观 -> 小工具
向右边适当的位置添加一个 **文本** 或 **自定义HTML** 小工具,内容填写上述代码即可
[高级用法](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/advance-usage.md)
## 🛠️ 如何部署
想要自己定制代码?嫌我提供的服务太慢?
你也可以轻松拥有完全属于自己的小挂件!
> 需要 Docker 或 PHP 版本 >= 5.6
[部署文档](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.md)
## 🔌 API
[排行榜数据(已上传至图床)](https://pixiv.mokeyjay.com/?r=api/pixiv-json)(推荐)
[排行榜数据(pixiv url)](https://pixiv.mokeyjay.com/?r=api/source-json)
其中 `data` 为排行榜数据;`date` 为排行榜日期(可能是昨天或者前天,因为官方更新时间不一定)
这两个接口都会自动根据请求头的 `Origin` 或者 `Referer` 返回对应跨域头。可供前端直接调用
> `image` 和 `url` 两个键是为了兼容 4.x 及之前版本的用户,无需理会
## 🆙 升级指南
### 从 5.2 升级到 6.0
1. [下载源代码](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/releases/latest)
2. 解压缩,将其中的 `app` 和 `index.php` 覆盖到线上环境
> **⚠️ 对于 Docker 方式部署的用户**
> - 请将环境变量名中所有 `-` 替换为 `_`
> - 镜像从 docker hub 迁移至 [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)
## 🌟 更新日志
### 新增
- `ranking_type` 配置项,现在可以选择拉取综合还是插画、漫画日榜啦~
- 图片预加载,优化网络环境较差时的体验
### 优化
- 完全重写了前端,更优雅的缓动效果
- 不再依赖 bootstrap,加载更快啦
- 改为使用官方 php、nginx 包
- 从本地存储获取图片时不再重复检查完整性
- `background_color` 配置项现在支持[更多种颜色](https://developer.mozilla.org/zh-CN/docs/Web/CSS/background-color)了
### 修复
- 部分环境变量在一些情况下无法被正常获取的问题
- 定时任务实际上是一小时执行一次,而非文档说的半小时一次
### 其他
- 由于不再依赖 bs,去除 `static_cdn` 配置项
- 删除已经失效的 `Pngcm`、`Tsesze` 图床
- Docker 镜像迁移至 [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)
- Docker 镜像时区默认为上海
[历史更新日志](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/log.md)
## 👨💻 关于作者
常用 ID [mokeyjay](https://www.mokeyjay.com),热爱 IT 与 ACG 的学渣
================================================
FILE: app/App.php
================================================
getMessage(), 'ERROR');
http_response_code(500);
die;
});
}
public static function run()
{
self::init();
self::job();
self::route();
}
/**
* 运行任务
* @throws \Exception
*/
protected static function job()
{
$opt = getopt('j:');
if (empty($_GET['job']) && empty($opt['j'])) {
return;
}
$jobName = !empty($_GET['job']) ? $_GET['job'] : $opt['j'];
$job = Job::make($jobName);
if (!empty($_GET['job']) && $job->onlyActivateByCli) {
throw new \Exception("任务 {$jobName} 只能通过 cli 触发");
}
if (!$job) {
throw new \Exception("任务 {$jobName} 加载失败");
}
set_time_limit(0);
if ($job->run()) {
Log::write("任务 {$jobName} 执行完毕");
echo "任务 {$jobName} 执行完毕";
} else {
throw new \Exception("任务 {$jobName} 执行失败:{$job->getErrorMsg()}");
}
exit;
}
/**
* 路由
*/
protected static function route()
{
$route = isset($_GET['r']) ? $_GET['r'] : 'index';
$route = explode('/', $route);
$controller = Str::studly(array_shift($route) ?: 'index');
$method = lcfirst(Str::studly(array_pop($route) ?: 'index'));
$class = "app\\Controllers\\{$controller}Controller";
if (!class_exists($class)) {
Log::write("控制器不存在:{$class}", Log::LEVEL_ERROR);
http_response_code(404);
die;
}
$controller = new $class;
if (!is_callable([$controller, $method])) {
Log::write("无法调用此方法:{$class}->{$method}()", Log::LEVEL_ERROR);
http_response_code(404);
die;
}
$controller->$method();
}
}
================================================
FILE: app/Controllers/ApiController.php
================================================
setAutoCorsHeader();
}
/**
* 自动设置跨域头
* @return void
*/
private function setAutoCorsHeader()
{
if (!isset($_SERVER['HTTP_ORIGIN']) && !isset($_SERVER['HTTP_REFERER'])) {
return;
}
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : $_SERVER['HTTP_REFERER'];
header('Access-Control-Allow-Origin: ' . $origin);
}
/**
* 源数据(未使用图床)
* @return void
*/
public function sourceJson()
{
echo json_encode(Storage::getJson('source') ?: []);
}
/**
* 处理后的数据(已使用图床)
* @return void
*/
public function pixivJson()
{
echo json_encode(Storage::getJson('pixiv') ?: []);
}
}
================================================
FILE: app/Controllers/Controller.php
================================================
errorMsg;
}
}
================================================
FILE: app/ImageHosting/Catbox.php
================================================
'fileupload',
'userhash' => '',
'fileToUpload' => Curl::getCurlFile($path),
];
$result = Curl::post('https://catbox.moe/user/api.php', $data);
Log::write('[猫盒图床]上传:' . json_encode($data));
Log::write('[猫盒图床]返回:' . $result);
if (stripos($result, 'files.catbox.moe') !== false) {
return $result;
}
Log::write('[猫盒图床]上传失败', 'ERROR');
return false;
}
}
================================================
FILE: app/ImageHosting/Chkaja.php
================================================
Curl::getCurlFile($path),
'viewpassword' => '',
'viewtips' => '',
'Timelimit' => '',
'Countlimit' => '',
'submit' => 'submit',
'MAX_FILE_SIZE' => '8388608',
];
$result = Curl::post('https://img.chkaja.com/ajaximg.php', $data);
Log::write('[愛上傳图床]上传:' . json_encode($data));
Log::write('[愛上傳图床]返回:' . $result);
$url = preg_match('|https://img.chkaja.com/(.*?)\.jpg|', $result, $matches) ? $matches[0] : false;
if (!empty($url)) {
return $url;
}
Log::write('[愛上傳图床]上传失败', 'ERROR');
return false;
}
}
================================================
FILE: app/ImageHosting/FiftyEight.php
================================================
'0*0',
'Pic-Encoding' => 'base64',
'Pic-Path' => '/nowater/webim/big/',
'Pic-Data' => base64_encode(file_get_contents($path)),
];
$result = Curl::post('https://upload.58cdn.com.cn/json', json_encode($data), [
CURLOPT_HTTPHEADER => [
'Origin: https://ai.58.com',
'Referer: https://ai.58.com/pc/'
],
]);
$data['Pic-Data'] = '(数据长度:' . strlen($data['Pic-Data']) . ')';
Log::write('[58图床]上传:' . json_encode($data));
Log::write('[58图床]返回:' . $result);
if (empty($result) || stripos($result, 'n_v2') !== 0) {
Log::write('[58图床]上传失败', 'ERROR');
return false;
}
return 'https://pic3.58cdn.com.cn/nowater/webim/big/' . $result;
}
}
================================================
FILE: app/ImageHosting/ImageHosting.php
================================================
pathinfo($path, PATHINFO_BASENAME),
'uuid' => 'o_1g' . Str::random(27),
'uploadPath' => $config['upload_path'],
'mode' => 1,
'file' => Curl::getCurlFile($path),
];
$result = Curl::post(rtrim($config['url']) . '/file.php', $data, [
CURLOPT_HTTPHEADER => [
'accept: */*',
'referer: ' . $config['url'],
'origin: ' . $config['url'],
],
CURLOPT_COOKIE => sprintf('frontendlogin=y; name-mode=_isRenameMode; filemanager%s=%s', $config['unique_id'], $config['token']),
]);
Log::write('[薄荷图床]上传:' . json_encode($data));
Log::write('[薄荷图床]返回:' . $result);
$json = json_decode($result, true);
if (isset($json['result']) && $json['result'] == 'success' && !empty($json['url'])) {
return $json['url'];
}
Log::write('[薄荷图床]上传失败', 'ERROR');
return false;
}
}
================================================
FILE: app/ImageHosting/Smms.php
================================================
Curl::getCurlFile($path)];
$header = [
CURLOPT_HTTPHEADER => [
'Authorization' => Config::$image_hosting_extend['smms']['token'],
],
];
$result = Curl::post('https://sm.ms/api/v2/upload', $data, $header);
Log::write('[Smms图床]上传:' . json_encode($data));
Log::write('[Smms图床]返回:' . $result);
$result = json_decode($result, true);
if (isset($result['code'])) {
if ($result['code'] == 'success') {
return $result['data']['url'];
} elseif ($result['code'] == 'image_repeated') {
return $result['images'];
}
}
Log::write('[Smms图床]上传失败:' . $result['msg'], 'ERROR');
return false;
}
}
================================================
FILE: app/ImageHosting/Tietuku.php
================================================
Config::$image_hosting_extend['tietuku']['token'],
'file' => Curl::getCurlFile($path),
];
$result = Curl::post('http://up.imgapi.com/', $data);
Log::write('[Tietuku图床]上传:' . json_encode($data));
Log::write('[Tietuku图床]返回:' . $result);
$result = json_decode($result, true);
if (isset($result['code'])) {
Log::write('[Tietuku图床]上传失败:' . $result['info'], 'ERROR');
return false;
}
return $result['linkurl'];
}
}
================================================
FILE: app/Jobs/ClearLog.php
================================================
0 && is_numeric($opt['n'])) ? intval($opt['n']) : 7;
$time = strtotime("-{$n} days");
$path = STORAGE_PATH . 'logs/';
$deleteNum = 0;
if ($dh = opendir($path)) {
while (($file = readdir($dh)) !== false) {
if (in_array($file, ['.', '..', '.gitignore'])) {
continue;
}
$file = $path . $file;
if (filemtime($file) < $time) {
$deleteNum++;
Storage::deleteFile($file);
}
}
}
Log::write("共计清除日志文件 {$deleteNum} 个");
return true;
}
}
================================================
FILE: app/Jobs/Job.php
================================================
needRefresh($ranking, $pixivJson)){
Log::write('排行榜尚未更新,半小时后再试');
Lock::forceCreate('refresh', 1800);
return true;
}
$images = Pixiv::getImages();
if($images === false) {
throw new \Exception('【致命错误】无法获取Pixiv排行榜图片列表');
}
$enableCompress = Config::$compress && function_exists('imagecreatefromjpeg');
$imageHostingInstances = [];
foreach (Config::$image_hosting as $ihName) {
$imageHostingInstances[] = ImageHosting::make($ihName);
}
$proxy = Config::$proxy;
// 开始获取图片
$pixivJson = [];
foreach ($images['data'] as $i => $data) {
// 缓存数量限制
if ($i >= Config::$limit) {
break;
}
Log::write("开始获取第 " . ($i + 1) . " 张图:{$data['url']}");
// 最多尝试下载 3 次
Config::$proxy = $proxy;
for ($ii = 1; $ii <= 3; $ii++) {
$tmpfile = Pixiv::downloadImage($data['url']);
if ($tmpfile && getimagesize($tmpfile)) {
break;
} else {
Log::write("图片 {$data['url']} 下载失败,重试第 {$ii} 次");
sleep(mt_rand(3, 30));
}
}
if (!$tmpfile) {
throw new \Exception("图片 {$data['url']} 下载失败");
}
// 压缩图片
if ($enableCompress) {
$image = imagecreatefromjpeg($tmpfile);
if ($image) {
imagejpeg($image, $tmpfile, 95);
$bytes = filesize($tmpfile);
Log::write('压缩后图片大小: ' . $bytes . ' 字节');
imagedestroy($image);
unset($image);
}
if ($bytes < 1000) {
throw new \Exception("图片 {$data['url']} 下载失败");
}
}
// 上传到图床
Config::$proxy = null; // 上传过程中禁用代理
foreach ($imageHostingInstances as $imageHosting) {
$url = $imageHosting->upload($tmpfile);
if ($url != false) {
Storage::deleteFile($tmpfile);
break;
}
}
$url = $url ?: Pixiv::getProxyUrl($data['url']); // 如上传失败则使用反代 url
$data['url'] = $url;
$pixivJson['data'][] = $data;
$pixivJson['image'][] = $url;
$pixivJson['url'][] = "artworks/{$data['id']}";
}
$pixivJson['date'] = $images['date'];
Storage::saveJson('pixiv', $pixivJson);
Lock::remove('refresh');
Config::$clear_overdue && Storage::clearOverdueImages();
return true;
} catch (\Exception $e) {
// 是否超过最大重试次数
$refreshCount = (int)Storage::get('refreshCount');
if ($refreshCount > 10) {
// 超过 10 次(5小时)都无法获取到 pixiv 排行榜
// 直接锁定一整天,明天再试,降低无意义的资源损耗
$expire = mktime(23, 59, 59) - time();
Lock::forceCreate('refresh', $expire);
Storage::remove('refreshCount');
} else {
// 半小时后再试
Lock::forceCreate('refresh', 1800);
Storage::save('refreshCount', $refreshCount + 1);
}
$this->errorMsg = $e->getMessage();
return false;
}
}
/**
* 是否需要刷新数据
* @param array $ranking pixiv 接口返回的排行榜数据
* @param array $pixivJson pixiv.json 的内容
* @return bool
* @throws \Exception
*/
private function needRefresh($ranking, $pixivJson)
{
if (!isset($pixivJson['date'])) {
return true;
}
// $ranking['date'] 的格式为 20200310
if ($ranking && isset($ranking['date']) && preg_match('|^\d{8}$|', $ranking['date'])) {
return $pixivJson['date'] != date('Y-m-d', strtotime($ranking['date']));
}
throw new \Exception('排行榜日期数据异常!数据:' . json_encode($ranking));
}
}
================================================
FILE: app/Libs/Config.php
================================================
$value) {
self::$$key = $value;
}
// 获取本项目 url
if (self::$url == '' && !IS_CLI) {
self::$url = Request::getCurrentUrl();
}
// 是否对外提供服务,是则获取 url 参数
if (self::$service) {
if (isset($_GET['color'])) {
self::$background_color = '#' . $_GET['color'];
}
if (isset($_GET['limit'])) {
self::$limit = (int)$_GET['limit'];
}
}
try {
if (!is_writable(STORAGE_PATH)) {
throw new \Exception(STORAGE_PATH . ' 目录无法写入');
}
if (!is_array(Config::$image_hosting) || count(Config::$image_hosting) < 1) {
throw new \Exception('image_hosting 配置项至少要有一个值');
}
if (self::$limit < 1) {
throw new \Exception('limit 配置项不得小于1');
}
if (IS_CLI && self::$url == '' && in_array('local', self::$image_hosting)) {
throw new \Exception('在 cli 模式下使用 local 本地图床时,必须配置 url 项,否则可能会生成错误的缩略图 url');
}
if (!in_array(Config::$ranking_type, ['', 'illust', 'manga'])) {
throw new \Exception('ranking_type 配置项必须为空、illust 或 manga');
}
} catch (\Exception $e) {
Log::write($e->getMessage(), Log::LEVEL_ERROR);
echo '错误:' . $e->getMessage();
die;
}
}
}
================================================
FILE: app/Libs/Curl.php
================================================
'', 'html' => ''] 数组
*/
public static function get($url, $opt = [], $includeCookie = false)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.52');
Config::$proxy && curl_setopt($ch, CURLOPT_PROXY, Config::$proxy);
$includeCookie && curl_setopt($ch, CURLOPT_HEADER, true);
if (count($opt)) {
curl_setopt_array($ch, $opt);
}
$data = curl_exec($ch);
curl_close($ch);
if ($includeCookie) {
$data = explode("\r\n\r\n", $data);
$header = array_shift($data);
$html = implode("\r\n\r\n", $data);
preg_match_all("/set-cookie:([^\r\n]*)/i", $header, $matches);
$cookie = '';
if (!empty($matches[1])) {
$cookieItem = [];
foreach ($matches[1] as $item) {
$cookieItem[] = explode(';', trim($item))[0];
}
$cookie = implode('; ', $cookieItem);
}
return compact('cookie', 'html');
}
return $data;
}
/**
* post请求
* @param string $url
* @param array|string $postData
* @param array $opt
* @return bool|string
*/
public static function post($url, $postData, $opt = [])
{
$opt[CURLOPT_POST] = true;
$opt[CURLOPT_POSTFIELDS] = $postData;
return self::get($url, $opt);
}
/**
* 获取curl file实例
* @param string $path 文件路径
* @return \CURLFile|string
*/
public static function getCurlFile($path)
{
return class_exists('CURLFile') ? (new \CURLFile(realpath($path))) : ('@' . realpath($path));
}
}
================================================
FILE: app/Libs/Env.php
================================================
time() || $lock == 0);
}
/**
* 创建锁
* @param string $name
* @param int $expire 自动过期时间(秒)
* @return bool 创建失败或锁已存在时返回 false
*/
public static function create($name, $expire = 0)
{
if (self::check($name) === false) {
return Storage::save("app/{$name}.lock", ($expire ? (time() + $expire) : 0));
}
return false;
}
/**
* 移除锁
* @param string $name
* @return bool
*/
public static function remove($name)
{
return Storage::remove("app/{$name}.lock");
}
/**
* 强制创建锁
* @param string $name 锁名称
* @param int $expire 自动过期时间(秒)
* @return bool
*/
public static function forceCreate($name, $expire)
{
return Storage::save("app/{$name}.lock", ($expire ? (time() + $expire) : 0));
}
}
================================================
FILE: app/Libs/Log.php
================================================
{$message}\n";
if (IS_CLI) {
echo $content . "\n";
}
$file = STORAGE_PATH . 'logs/' . date('Ymd') . (IS_CLI ? '-cli' : '') . '.log';
return file_put_contents($file, $content, FILE_APPEND) !== false;
}
}
================================================
FILE: app/Libs/Pixiv.php
================================================
'daily',
'p' => $page,
'format' => 'json',
];
if (Config::$ranking_type) {
$params['content'] = Config::$ranking_type;
}
$response = Curl::get('https://www.pixiv.net/ranking.php?' . http_build_query($params), [
CURLOPT_HTTPHEADER => [
'Referer: https://www.pixiv.net/ranking.php?mode=daily',
],
]);
$json = json_decode($response, true);
if (!isset($json['contents'])) {
Log::write('获取排行榜数据失败!接口返回值:' . $response, Log::LEVEL_ERROR);
return false;
}
return $json;
}
/**
* 获取图片url列表
* @return array|false
* @throws \Exception
*/
public static function getImages()
{
$source = Storage::getJson('source');
if (is_array($source) && self::checkDate($source)) {
return $source;
}
$picNum = 0;
$sourceJson = [];
for ($page = 1; $page <= 10; $page++) {
$json = self::getRanking($page);
if($json === false){
return false;
}
foreach ($json['contents'] as $item) {
$sourceJson['data'][] = [
'id' => $item['illust_id'],
'url' => $item['url'],
'title' => $item['title'],
'tags' => $item['tags'],
'width' => $item['width'],
'height' => $item['height'],
'page_count' => $item['illust_page_count'],
'rank' => $item['rank'],
'yesterday_rank' => $item['yes_rank'],
'user_id' => $item['user_id'],
'user_name' => $item['user_name'],
'uploaded_at' => $item['illust_upload_timestamp'],
];
// image 和 url 是为了兼容 5.x 之前的旧版本
$sourceJson['image'][] = $item['url'];
$sourceJson['url'][] = "artworks/{$item['illust_id']}";
$picNum++;
if ($picNum >= Config::$limit) {
break 2;
}
}
}
$sourceJson['date'] = date('Y-m-d', strtotime($json['date']));
Storage::saveJson('source', $sourceJson);
return $sourceJson;
}
/**
* 下载 Pixiv 缩略图。成功返回临时文件名
* @param string $url
* @return string 临时文件名
*/
public static function downloadImage($url)
{
// 如果 local storage 已经存有这张图(每日榜上的图片是可能存在重复的),就不再重新下载了
$image = Storage::getImage(pathinfo($url, PATHINFO_BASENAME));
$shouldCheckComplete = !$image;
if ($image === false) {
$image = Curl::get($url, [
CURLOPT_HTTPHEADER => [
'Referer: https://www.pixiv.net/ranking.php?mode=daily',
],
]);
Log::write('下载到数据包大小: ' . strlen($image) . ' 字节');
}
if ($image) {
$file = explode('/', $url);
$file = array_pop($file);
$file = sys_get_temp_dir() . '/' . Str::random(16) . $file;
$bytes = file_put_contents($file, $image);
Log::write("写入文件 {$file} 大小:{$bytes} 字节");
// 检查文件是否下载完整
if ($shouldCheckComplete) {
$response = Curl::get($url, [
CURLOPT_NOBODY => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [
'Referer: https://www.pixiv.net/ranking.php?mode=daily',
],
]);
$contentLength = Response::getContentLength($response);
if ($bytes != $contentLength) {
Log::write("写入的文件大小与目标 content-length: {$contentLength} 不符");
return false;
}
}
return $bytes > 0 ? $file : false;
}
return false;
}
/**
* 检查传入数组的 date 值是否有效(即大于等于昨天)。返回 true 为未过期
* @param array $data
* @return bool
*/
public static function checkDate(array $data)
{
if(isset($data['date'])){
$yesterday = date('Y-m-d', strtotime('-1 day'));
return $data['date'] >= $yesterday;
}
return false;
}
/**
* 获取基于 pixiv.cat 提供的代理服务的图片 url
* 可以直接展示在页面上,突破 pixiv 的反盗链
* @param $url
* @return array|string|string[]
*/
public static function getProxyUrl($url)
{
return str_replace('i.pximg.net', 'i.pixiv.re', $url);
}
}
================================================
FILE: app/Libs/Request.php
================================================
1,
]);
}
}
================================================
FILE: app/Libs/Response.php
================================================
Pixiv 每日排行榜 Top= app\Libs\Config::$limit ?> 小挂件
================================================
FILE: app/Views/loading.php
================================================
Pixiv 每日排行榜 Top=Config::$limit?> 小挂件
================================================
FILE: app/autoload.php
================================================
'',
/**
* 背景颜色。默认值为 transparent (透明)。你也可以通过 url 参数 color 来设置
* Background color. The default value is transparent. You can also set the background color by url parameter 'color'
*/
'background_color' => 'transparent',
/**
* 显示和缓存的图片最大数量(范围1-500)
* 例如将此值设为 10 则可以做出 Top10 的效果
* 也可防止部分辣鸡主机在缓存图片时占用过多资源导致卡死或报警
* 一般情况下默认的 50 就行
*
* Maximum number of images to display and cache (range 1-500)
* For example, if you set this value to 10, you can make a Top10 effect
* It also prevents some low-performance hosts from using too many resources when caching images, which can lead to jamming or alarms
* Usually the default 50 is fine
*/
'limit' => 50,
/**
* 是否对外提供服务
* 为 true 时,任何人都可通过 url 的 get 参数来临时修改 background_color 和 limit 的值
*
* Whether to provide external services
* When true, anyone can temporarily change the values of background_color and limit by the url parameter
*/
'service' => true,
/**
* 日志级别。可多选:DEBUG、ERROR 或留空不记录任何日志
* Logging level. Multiple options: DEBUG, ERROR or leave blank to not record any logs
*/
'log_level' => [],
/**
* 代理服务器配置。例如 127.0.0.1:1080
* 留空为不使用代理
*
* Proxy server configuration. For example 127.0.0.1:1080
* Leave blank to not use proxy
*/
'proxy' => '',
/**
* 每次更新排行榜数据后,自动删除过期的本地缓存缩略图
* Automatically delete expired local cache thumbnails after each ranking data update
*/
'clear_overdue' => true,
/**
* 压缩缩略图,在几乎不损失画质的前提下减小 50% 左右的体积,降低服务器带宽压力
* 需要启用 PHP 的 GD 扩展
*
* Compress thumbnails to reduce the size by about 50% with almost no loss of image quality, reducing server bandwidth pressure
* Need the GD extension for PHP
*/
'compress' => true,
/**
* 图床名称
* (推荐度按照顺序从高到低)
*
* 推荐填写多个图床,如果其中一个图床上传失败,则将按照顺序继续尝试其他图床
*
* Image-Hosting
* (Recommendation is ranked from highest to lowest)
*
* It is recommended to fill in more than one image-hosting, if one of them fails to upload, it will continue to try other image-hosting in order
*/
'image_hosting' => ['fifty-eight', 'chkaja', 'catbox', 'local'],
/**
* 图床扩展配置信息
* Extend Configuration information for the image-hosting
*/
'image_hosting_extend' => [
'tietuku' => [
'token' => ''
],
'smms' => [
'token' => '',
],
// 薄荷图床
'riyugo' => [
// 客服提供的会员专属网址。例如 https://r789.com/1234,必须以 / 结尾
'url' => '',
// 要上传到的文件夹。通常可以留空
'upload_path' => '',
// 管理后台-设置 中的 唯一用户ID
'unique_id' => '',
// 登录管理后台后,filemanagerXXXXXXXXX 这个 cookie 的值
// (XXXXXXXX 是你的唯一用户 ID)
'token' => '',
],
],
/**
* 禁用 web 访问的方式触发 job 更新,仅限 cli 方式触发
* 由于部分环境 web 超时时间不够,会导致更新操作不断被触发但又无法完成整个更新流程
* 因此添加一个开关,避免 web 触发更新,节约服务器资源
*
* Disable web-trigger update job, cli-trigger only
* See doc/deploy.en.md
*/
'disable_web_job' => false,
/**
* 放置在页面 标签下的 js 脚本内容,通常用来放置统计代码
* 无需