[
  {
    "path": ".docker/Dockerfile",
    "content": "FROM ghcr.io/mokeyjay/pixiv-widget-image:main\n\nCOPY . /var/www/html\nCOPY .docker/config.php /var/www/html/config.php\nCOPY .docker/nginx-site.conf /etc/nginx/conf.d/default.conf\nCOPY .docker/start.sh /root/start.sh\n\nCOPY .docker/crontab /etc/cron.d/pixiv-cron-job\nRUN chmod 0644 /etc/cron.d/pixiv-cron-job && \\\n    crontab /etc/cron.d/pixiv-cron-job\n\nENV TZ=Asia/Shanghai\n\nLABEL Author=\"mokeyjay<i@mokeyjay.com>\"\nLABEL Version=\"2023.11.20\"\nLABEL Description=\"Pixiv 每日排行榜小挂件\"\n\nCMD cron && \\\n    chmod +x /root/start.sh && \\\n    /root/start.sh\n"
  },
  {
    "path": ".docker/config.php",
    "content": "<?php\n/**\n * 此文件为 docker 环境的配置文件\n * 每个参数的含义见 config.php\n *\n * This file is the docker environment configuration file\n * See config.php for the meaning of each parameter\n */\n\nuse app\\Libs\\Env;\n\nEnv::init();\n\nreturn [\n    'url' => Env::getStr('URL'),\n\n    'background_color' => Env::getStr('BACKGROUND_COLOR', 'transparent'),\n\n    'limit' => Env::getStr('LIMIT', 50),\n\n    'service' => Env::getBool('SERVICE', true),\n\n    'log_level' => Env::getArray('LOG_LEVEL', ['DEBUG', 'ERROR']),\n\n    'proxy' => Env::getStr('PROXY'),\n\n    'clear_overdue' => Env::getBool('CLEAR_OVERDUE', true),\n\n    'compress' => Env::getBool('COMPRESS', true),\n\n    'image_hosting' => Env::getArray('IMAGE_HOSTING', ['local']),\n\n    'image_hosting_extend' => [\n        'tietuku' => [\n            'token' => Env::getStr('IMAGE_HOSTING_EXTEND_TIETUKU_TOKEN'),\n        ],\n        'smms' => [\n            'token' => Env::getStr('IMAGE_HOSTING_EXTEND_SMMS_TOKEN'),\n        ],\n        'riyugo' => [\n            'url' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_URL'),\n            'upload_path' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_UPLOAD_PATH'),\n            'unique_id' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_UNIQUE_ID'),\n            'token' => Env::getStr('IMAGE_HOSTING_EXTEND_RIYUGO_TOKEN'),\n        ],\n    ],\n\n    'disable_web_job' => Env::getBool('DISABLE_WEB_JOB', true),\n\n    'header_script' => Env::getStr('HEADER_SCRIPT', ''),\n\n    'ranking_type' => Env::getStr('RANKING_TYPE', ''),\n];"
  },
  {
    "path": ".docker/crontab",
    "content": "*/30 * * * * cd /var/www/html/ && /usr/local/bin/php index.php -j=refresh\n30 1 * * * cd /var/www/html/ && /usr/local/bin/php index.php -j=clear-log\n"
  },
  {
    "path": ".docker/nginx-site.conf",
    "content": "server {\n    listen       80;\n    listen  [::]:80;\n    server_name  _;\n    root /var/www/html;\n    index index.php;\n\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n\n    location ~ /\\. {\n       deny all;\n    }\n\n    location / {\n        try_files $uri $uri/ /index.php?$query_string;\n    }\n\n    location ~ \\.php$ {\n        fastcgi_pass   127.0.0.1:9000;\n        fastcgi_index  index.php;\n        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n        fastcgi_param  PATH_INFO        $fastcgi_path_info;\n        fastcgi_param  PATH_TRANSLATED  $document_root$fastcgi_path_info;\n        include        fastcgi_params;\n    }\n}\n"
  },
  {
    "path": ".docker/start.sh",
    "content": "#!/bin/bash\n\necho '' > /etc/nginx/sites-enabled/default\n\ncd /var/www/html\n# 将环境变量保存起来，免得 crontab 读不到\nenv=(\"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\")\nfile=\".env\"\n> $file\nfor var in \"${env[@]}\"\ndo\n  # 部分环境变量值含有空格、换行，得用双引号包起来，不然 source 时会报错\n  value=\"${!var}\"\n  value=\"${value//\\\"/\\\\\\\"}\"\n  echo \"$var=\\\"$value\\\"\" >> $file\ndone\n\nchown -R www-data:www-data /var/www/html\n\nphp-fpm -D\nnginx -g 'daemon off;'"
  },
  {
    "path": ".github/workflows/develop.yml",
    "content": "name: 推送开发用的 docker 镜像\non:\n  push:\n    branches:\n      - 'develop'\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  push_to_registry:\n    name: 构建并推送\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      # https://github.com/actions/checkout\n      - name: 拉取代码\n        uses: actions/checkout@v4\n        with:\n          ref: develop\n\n      # https://github.com/docker/login-action\n      - name: 登录到 ghcr\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      # https://github.com/docker/metadata-action\n      - name: 提取事件元数据\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      # https://github.com/docker/setup-buildx-action\n      - name: 使用 buildx 作为构建器\n        uses: docker/setup-buildx-action@v3\n\n      # https://github.com/docker/build-push-action\n      - name: 构建并推送\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: .docker/Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          platforms: linux/amd64,linux/arm64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max"
  },
  {
    "path": ".github/workflows/docker-image.yml",
    "content": "# action 的名称\nname: 推送 Docker 镜像\n# 触发 action 的事件\non:\n  push:\n    # master 分支有推送时触发\n    branches:\n      - 'master'\n    # tag 新建时触发\n    tags:\n      - '*'\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  push_to_registry:\n    name: 构建并推送\n    # 基于指定平台构建。有 win、ubuntu、mac 可选\n    # 消耗的分钟倍数：linux 1x、win 2x、mac 10x\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      # https://github.com/actions/checkout\n      - name: 拉取代码\n        uses: actions/checkout@v4\n\n      # https://github.com/docker/login-action\n      - name: 登录到 ghcr\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      # 从触发此次 action 的事件中提取源数据（tag、label 什么的）\n      # https://github.com/docker/metadata-action\n      - name: 提取事件元数据\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          # 提取出来的源数据用于这个镜像\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      # 使用 buildx 作为构建器，以支持多平台构建之类的能力\n      # https://github.com/docker/setup-buildx-action\n      - name: 使用 buildx 作为构建器\n        uses: docker/setup-buildx-action@v3\n\n      # https://github.com/docker/build-push-action\n      - name: 构建并推送\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: .docker/Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max"
  },
  {
    "path": ".gitignore",
    "content": "/.idea\n.DS_Store\n"
  },
  {
    "path": ".test/test_image_hosting.php",
    "content": "<?php\n\nuse app\\Libs\\Config;\nuse app\\ImageHosting\\ImageHosting;\n\ndefine(\"BASE_PATH\", dirname(__FILE__, 2) . DIRECTORY_SEPARATOR);\nconst APP_PATH = BASE_PATH . 'app' . DIRECTORY_SEPARATOR;\nconst STORAGE_PATH = BASE_PATH . 'storage' . DIRECTORY_SEPARATOR;\nconst IS_CLI = PHP_SAPI === 'cli';\nconst TEST_PATH = BASE_PATH . '.test' . DIRECTORY_SEPARATOR;\n\nrequire APP_PATH . 'autoload.php';\n\nConfig::init();\n\n$skip = ['Riyugo', 'Smms', 'Tietuku', 'ImageHosting', 'Local', '.', '..'];\nforeach (scandir(APP_PATH . 'ImageHosting') as $fileName) {\n\n    $className = pathinfo($fileName, PATHINFO_FILENAME);\n    if (in_array($className, $skip) || empty($className)) {\n        continue;\n    }\n\n    $imageHosting = ImageHosting::make($className);\n    if ($url = $imageHosting->upload(TEST_PATH . 'pic.jpg')) {\n        var_dump($className . '成功： ' . $url);\n    } else {\n        var_dump($className . '失败： ' . $url);\n    }\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 mokeyjay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.en.md",
    "content": "<h1 align=\"center\">🖼️ Pixiv Daily Ranking Widget</h1>\n<p align=\"center\">\n    <a href=\"https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/README.md\">中文</a>\n    <br><br>\n    Want to add a <span style=\"font-weight: bold\">Pixiv Daily Ranking Widget</span> to your website? It's a matter of one line of code!\n    <a href=\"https://pixiv.mokeyjay.com/demo.html\" target=\"_blank\">DEMO</a>\n</p>\n\n## ✨ Features\n- Easy to use with one line of `HTML` code\n- Adaptive width and height. A minimum size of `240px * 380px` (width \\* height) or more is recommanded\n- Redirect to artwork page by clicking on the widget\n- Automatic daily update\n- Low system resource utilisation with supports for multiple image hosting platforms and on-demand image loading\n- Offering an API that includes ranking update date, thumbnail URL and detail page URL etc.\n\n## 🤔 How to use\nJust add the below code to your page\n```html\n<iframe src=\"https://pixiv.mokeyjay.com\" style=\"width:240px; height:380px; border: 0\"></iframe>\n```\n\nTaking `WordPress` as an example. On `wp-admin`, click on **Appearance** -> **Widgets**  \nThen add a **Text** or **Custom HTML** widget as deemed appropriate on the right and fill the code above in\n\n[Advance Usage](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/advance-usage.en.md)\n\n## 🛠️ How to deploy\nWanted to customize the code yourself? Thought the service I provided is slow in speed?  \n\nYou can also easily deploy your own widget!\n> Requires Docker or PHP version >= 5.6\n\n[Deployment Documentation](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.en.md)\n\n## 🔌 APIs\n[Ranking data (images hosted privately)](https://pixiv.mokeyjay.com/?r=api/pixiv-json) (recommended)  \n[Ranking data (pixiv url)](https://pixiv.mokeyjay.com/?r=api/source-json)\n\nIn 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)  \n\nBoth APIs automatically return the respective cross-domain header according to `Origin` or `Referer` within the request header. The APIs are front-end ready.\n\n> The `image` and `url` keys are for compatibility purposes for users of 4.x or earlier versions, they can be ignored\n\n## 🆙 Upgrading Guide\n### Upgrading From 5.2 to 6.0\n1. [Download the Source Code](https://github.com/mokeyjay/pixiv-daily-ranking-widget/releases/latest)\n2. Unzip and overwrite the `app` and `index.php` to your server\n> **⚠️ For Docker User**\n> - Please replace all `-` in the environment variable name with `_`\n> - Docker Image migration from Docker Hub to [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)\n\n## 🌟 Changelog\n### New Features\n- Added `ranking_type` configuration option, which now allows you to select whether to fetch the overall or illustration/manga daily rankings.  \n- Added image preloading to improve the experience in poor network environments.\n### Optimizations\n- Completely rewritten the frontend with more elegant animation effects.  \n- Removed the dependency on Bootstrap for faster loading.  \n- Switched to using the official PHP and Nginx packages.\n- No longer repeatedly check integrity when retrieving images from local storage.\n- The `background_color` configuration now supports [more colors](https://developer.mozilla.org/en-US/docs/Web/CSS/background-color)\n### Fixes\n- Some environment variables cannot be obtained normally in some cases\n- The scheduled task actually runs once every hour, not every half hour as stated in the documentation\n### Other\n- Removed the `static_cdn` configuration option due to the removal of the dependency on Bootstrap.\n- Removed the invalid `Pngcm` and `Tsesze` image-hosting\n- Docker image migration to [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)\n- Docker image timezone defaults to Shanghai\n\n[History](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/log.en.md)\n\n## 👨‍💻 About author\n[mokeyjay](https://www.mokeyjay.com), IT and ACG lover\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">🖼️ Pixiv 每日排行榜小挂件</h1>\n<p align=\"center\">\n    <a href=\"https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/README.en.md\">English</a>\n    <br><br>\n    想要在你的网站页面中添加一个 <span style=\"font-weight: bold\">Pixiv 每日排行榜</span> 的展示功能吗？现在，只需要一行代码即可实现！\n    <a href=\"https://pixiv.mokeyjay.com/demo.html\" target=\"_blank\">在线预览</a>\n</p>\n\n## ✨ 特色\n- 一行 `HTML` 代码即可调用，方便快捷\n- 自适应宽高。推荐宽度 `240px`、高度 `380px` 或以上\n- 点击图片可跳转到对应作品详情页\n- 每日自动更新，无需人工干预\n- 内置多图床支持、按需加载图片，极低资源消耗\n- 提供 API 服务，含有排行榜更新日期、缩略图 url 及详情页 url 等\n\n## 🤔 如何使用\n将这行代码添加到网页上即可  \n```html\n<iframe src=\"https://pixiv.mokeyjay.com\" style=\"width:240px; height:380px; border: 0\"></iframe>\n```\n\n以 `Wordpress` 为例。首先进入后台，点击 外观 -> 小工具  \n向右边适当的位置添加一个 **文本** 或 **自定义HTML** 小工具，内容填写上述代码即可  \n\n[高级用法](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/advance-usage.md)\n\n## 🛠️ 如何部署\n想要自己定制代码？嫌我提供的服务太慢？  \n你也可以轻松拥有完全属于自己的小挂件！  \n> 需要 Docker 或 PHP 版本 >= 5.6\n\n[部署文档](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.md)\n\n## 🔌 API\n[排行榜数据（已上传至图床）](https://pixiv.mokeyjay.com/?r=api/pixiv-json)（推荐）  \n[排行榜数据（pixiv url）](https://pixiv.mokeyjay.com/?r=api/source-json)  \n\n其中 `data` 为排行榜数据；`date` 为排行榜日期（可能是昨天或者前天，因为官方更新时间不一定）  \n这两个接口都会自动根据请求头的 `Origin` 或者 `Referer` 返回对应跨域头。可供前端直接调用  \n\n> `image` 和 `url` 两个键是为了兼容 4.x 及之前版本的用户，无需理会\n\n## 🆙 升级指南\n### 从 5.2 升级到 6.0\n1. [下载源代码](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/releases/latest)\n2. 解压缩，将其中的 `app` 和 `index.php` 覆盖到线上环境\n> **⚠️ 对于 Docker 方式部署的用户**\n> - 请将环境变量名中所有 `-` 替换为 `_`\n> - 镜像从 docker hub 迁移至 [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)\n\n## 🌟 更新日志\n### 新增\n- `ranking_type` 配置项，现在可以选择拉取综合还是插画、漫画日榜啦~\n- 图片预加载，优化网络环境较差时的体验\n### 优化\n- 完全重写了前端，更优雅的缓动效果\n- 不再依赖 bootstrap，加载更快啦\n- 改为使用官方 php、nginx 包\n- 从本地存储获取图片时不再重复检查完整性\n- `background_color` 配置项现在支持[更多种颜色](https://developer.mozilla.org/zh-CN/docs/Web/CSS/background-color)了\n### 修复\n- 部分环境变量在一些情况下无法被正常获取的问题\n- 定时任务实际上是一小时执行一次，而非文档说的半小时一次\n### 其他\n- 由于不再依赖 bs，去除 `static_cdn` 配置项\n- 删除已经失效的 `Pngcm`、`Tsesze` 图床\n- Docker 镜像迁移至 [ghcr.io](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/pkgs/container/pixiv-daily-ranking-widget)\n- Docker 镜像时区默认为上海\n\n[历史更新日志](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/log.md)\n\n## 👨‍💻 关于作者\n常用 ID [mokeyjay](https://www.mokeyjay.com)，热爱 IT 与 ACG 的学渣"
  },
  {
    "path": "app/App.php",
    "content": "<?php\r\n\r\nnamespace app;\r\n\r\nuse app\\Jobs\\Job;\r\nuse app\\Libs\\Config;\r\nuse app\\Libs\\Log;\r\nuse app\\Libs\\Str;\r\n\r\nclass App\r\n{\r\n    /**\r\n     * 应用初始化\r\n     */\r\n    protected static function init()\r\n    {\r\n        Config::init();\r\n\r\n        // 注册全局错误捕捉\r\n        set_exception_handler(function ($exception) {\r\n            Log::write($exception->getMessage(), 'ERROR');\r\n            http_response_code(500);\r\n            die;\r\n        });\r\n    }\r\n\r\n    public static function run()\r\n    {\r\n        self::init();\r\n\r\n        self::job();\r\n\r\n        self::route();\r\n    }\r\n\r\n    /**\r\n     * 运行任务\r\n     * @throws \\Exception\r\n     */\r\n    protected static function job()\r\n    {\r\n        $opt = getopt('j:');\r\n        if (empty($_GET['job']) && empty($opt['j'])) {\r\n            return;\r\n        }\r\n\r\n        $jobName = !empty($_GET['job']) ? $_GET['job'] : $opt['j'];\r\n        $job = Job::make($jobName);\r\n\r\n        if (!empty($_GET['job']) && $job->onlyActivateByCli) {\r\n            throw new \\Exception(\"任务 {$jobName} 只能通过 cli 触发\");\r\n        }\r\n\r\n        if (!$job) {\r\n            throw new \\Exception(\"任务 {$jobName} 加载失败\");\r\n        }\r\n\r\n        set_time_limit(0);\r\n        if ($job->run()) {\r\n            Log::write(\"任务 {$jobName} 执行完毕\");\r\n            echo \"任务 {$jobName} 执行完毕\";\r\n        } else {\r\n            throw new \\Exception(\"任务 {$jobName} 执行失败：{$job->getErrorMsg()}\");\r\n        }\r\n\r\n        exit;\r\n    }\r\n\r\n    /**\r\n     * 路由\r\n     */\r\n    protected static function route()\r\n    {\r\n        $route = isset($_GET['r']) ? $_GET['r'] : 'index';\r\n        $route = explode('/', $route);\r\n\r\n        $controller = Str::studly(array_shift($route) ?: 'index');\r\n        $method = lcfirst(Str::studly(array_pop($route) ?: 'index'));\r\n\r\n        $class = \"app\\\\Controllers\\\\{$controller}Controller\";\r\n        if (!class_exists($class)) {\r\n            Log::write(\"控制器不存在：{$class}\", Log::LEVEL_ERROR);\r\n\r\n            http_response_code(404);\r\n            die;\r\n        }\r\n\r\n        $controller = new $class;\r\n        if (!is_callable([$controller, $method])) {\r\n            Log::write(\"无法调用此方法：{$class}->{$method}()\", Log::LEVEL_ERROR);\r\n\r\n            http_response_code(404);\r\n            die;\r\n        }\r\n\r\n        $controller->$method();\r\n    }\r\n}"
  },
  {
    "path": "app/Controllers/ApiController.php",
    "content": "<?php\n\nnamespace app\\Controllers;\n\nuse app\\Libs\\Storage;\n\n/**\n * 接口\n */\nclass ApiController extends Controller\n{\n    public function __construct()\n    {\n        header('content-type: application/json');\n\n        $this->setAutoCorsHeader();\n    }\n\n    /**\n     * 自动设置跨域头\n     * @return void\n     */\n    private function setAutoCorsHeader()\n    {\n        if (!isset($_SERVER['HTTP_ORIGIN']) && !isset($_SERVER['HTTP_REFERER'])) {\n            return;\n        }\n\n        $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : $_SERVER['HTTP_REFERER'];\n\n        header('Access-Control-Allow-Origin: ' . $origin);\n    }\n\n    /**\n     * 源数据（未使用图床）\n     * @return void\n     */\n    public function sourceJson()\n    {\n        echo json_encode(Storage::getJson('source') ?: []);\n    }\n\n    /**\n     * 处理后的数据（已使用图床）\n     * @return void\n     */\n    public function pixivJson()\n    {\n        echo json_encode(Storage::getJson('pixiv') ?: []);\n    }\n}"
  },
  {
    "path": "app/Controllers/Controller.php",
    "content": "<?php\n\nnamespace app\\Controllers;\n\nclass Controller\n{\n\n}"
  },
  {
    "path": "app/Controllers/IndexController.php",
    "content": "<?php\n\nnamespace app\\Controllers;\n\nuse app\\Libs\\Config;\nuse app\\Libs\\Lock;\nuse app\\Libs\\Pixiv;\nuse app\\Libs\\Request;\nuse app\\Libs\\Storage;\n\nclass IndexController extends Controller\n{\n    public function index()\n    {\n        $pixivJson = Storage::getJson('pixiv');\n        if ($pixivJson === false || !Pixiv::checkDate($pixivJson)) {\n            if (Lock::create('refresh', 600) && Config::$disable_web_job === false) {\n                Request::execRefreshThread();\n            }\n        }\n\n        if ($pixivJson === false) {\n            include APP_PATH . 'Views/loading.php';\n        } else {\n            $pixivJson['data'] = array_slice($pixivJson['data'], 0, Config::$limit);\n\n            require APP_PATH . 'Views/index.php';\n        }\n    }\n}"
  },
  {
    "path": "app/Factory.php",
    "content": "<?php\n\nnamespace app;\n\n/**\n * Class Factory\n * @package app\n */\nclass Factory\n{\n    public $errorMsg = '';\n\n    /**\n     * @param string $name   完整类名\n     * @param array  $config 参数\n     * @return mixed\n     */\n    public static function make($name, array $config = [])\n    {\n        try {\n            return new $name($config);\n        } catch (\\Exception $e) {\n            return false;\n        }\n\n    }\n\n    /**\n     * 获取错误信息\n     * @return string\n     */\n    public function getErrorMsg()\n    {\n        return $this->errorMsg;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/Catbox.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\n\n/**\n * 猫盒（美国）\n * Class Catbox\n * @package app\\ImageHosting\n * @url https://catbox.moe/\n */\nclass Catbox extends ImageHosting\n{\n    public function upload($path)\n    {\n        $data = [\n            'reqtype' => 'fileupload',\n            'userhash' => '',\n            'fileToUpload' => Curl::getCurlFile($path),\n        ];\n        $result = Curl::post('https://catbox.moe/user/api.php', $data);\n\n        Log::write('[猫盒图床]上传：' . json_encode($data));\n        Log::write('[猫盒图床]返回：' . $result);\n\n        if (stripos($result, 'files.catbox.moe') !== false) {\n            return $result;\n        }\n\n        Log::write('[猫盒图床]上传失败', 'ERROR');\n        return false;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/Chkaja.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\n\n/**\n * 愛上傳（Cloudflare）\n * Class Chkaja\n * @package app\\ImageHosting\n * @url https://img.chkaja.com/\n */\nclass Chkaja extends ImageHosting\n{\n    public function upload($path)\n    {\n        $data = [\n            'files[]' => Curl::getCurlFile($path),\n            'viewpassword' => '',\n            'viewtips' => '',\n            'Timelimit' => '',\n            'Countlimit' => '',\n            'submit' => 'submit',\n            'MAX_FILE_SIZE' => '8388608',\n        ];\n        $result = Curl::post('https://img.chkaja.com/ajaximg.php', $data);\n\n        Log::write('[愛上傳图床]上传：' . json_encode($data));\n        Log::write('[愛上傳图床]返回：' . $result);\n\n        $url = preg_match('|https://img.chkaja.com/(.*?)\\.jpg|', $result, $matches) ? $matches[0] : false;\n\n        if (!empty($url)) {\n            return $url;\n        }\n\n        Log::write('[愛上傳图床]上传失败', 'ERROR');\n        return false;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/FiftyEight.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\n\n/**\n * 58 同城图床（有时会和谐一些图）\n * 鸣谢：[@metowolf](https://github.com/metowolf)\n * Class FiftyEight\n * @package app\\ImageHosting\n */\nclass FiftyEight extends ImageHosting\n{\n    public function upload($path)\n    {\n        $data = [\n            'Pic-Size' => '0*0',\n            'Pic-Encoding' => 'base64',\n            'Pic-Path' => '/nowater/webim/big/',\n            'Pic-Data' => base64_encode(file_get_contents($path)),\n        ];\n        $result = Curl::post('https://upload.58cdn.com.cn/json', json_encode($data), [\n            CURLOPT_HTTPHEADER => [\n                'Origin: https://ai.58.com',\n                'Referer: https://ai.58.com/pc/'\n            ],\n        ]);\n\n        $data['Pic-Data'] = '（数据长度：' . strlen($data['Pic-Data']) . '）';\n        Log::write('[58图床]上传：' . json_encode($data));\n        Log::write('[58图床]返回：' . $result);\n\n        if (empty($result) || stripos($result, 'n_v2') !== 0) {\n            Log::write('[58图床]上传失败', 'ERROR');\n            return false;\n        }\n\n        return 'https://pic3.58cdn.com.cn/nowater/webim/big/' . $result;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/ImageHosting.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Factory;\nuse app\\Libs\\Str;\n\n/**\n * 抽象 图床类\n * Class ImageHosting\n * @package app\\ImageHosting\n */\nabstract class ImageHosting extends Factory\n{\n    /**\n     * @param string $name\n     * @param array  $config\n     * @return self\n     */\n    public static function make($name, array $config = [])\n    {\n        $name = '\\\\app\\\\ImageHosting\\\\' . Str::studly($name);\n\n        return parent::make($name, $config);\n    }\n\n    /**\n     * 上传图片。成功返回url，失败返回false\n     * @param string $path 图片路径\n     * @return string|false\n     */\n    abstract public function upload($path);\n}"
  },
  {
    "path": "app/ImageHosting/Local.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Config;\nuse app\\Libs\\Log;\n\n/**\n * 本地存储\n * Class Local\n * @package app\\ImageHosting\n * @url https://www.mokeyjay.com\n */\nclass Local extends ImageHosting\n{\n    public function upload($path)\n    {\n        $fileName = pathinfo($path, PATHINFO_BASENAME);\n        $file = STORAGE_PATH . 'images/' . $fileName;\n        Log::write('[本地]目标：' . $path);\n        Log::write('[本地]存储到：' . $file);\n\n        if (!file_put_contents($file, file_get_contents($path))) {\n            Log::write('[本地]存储失败', 'ERROR');\n            return false;\n        }\n\n        return Config::$url . 'storage/images/' . $fileName;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/Riyugo.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Config;\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\nuse app\\Libs\\Str;\n\n/**\n * 薄荷图床（国内，香港腾讯云）\n * Class riyugo\n * @package app\\ImageHosting\n * @url https://riyugo.com/\n */\nclass Riyugo extends ImageHosting\n{\n    public function upload($path)\n    {\n        $config = Config::$image_hosting_extend['riyugo'];\n        foreach (['url', 'unique_id', 'token'] as $key) {\n            if (empty($config[$key])) {\n                Log::write('[薄荷图床]读取账号配置失败');\n                return false;\n            }\n        }\n\n        $data = [\n            'name' => pathinfo($path, PATHINFO_BASENAME),\n            'uuid' => 'o_1g' . Str::random(27),\n            'uploadPath' => $config['upload_path'],\n            'mode' => 1,\n            'file' => Curl::getCurlFile($path),\n        ];\n        $result = Curl::post(rtrim($config['url']) . '/file.php', $data, [\n            CURLOPT_HTTPHEADER => [\n                'accept: */*',\n                'referer: ' . $config['url'],\n                'origin: ' . $config['url'],\n            ],\n            CURLOPT_COOKIE => sprintf('frontendlogin=y; name-mode=_isRenameMode; filemanager%s=%s', $config['unique_id'], $config['token']),\n        ]);\n\n        Log::write('[薄荷图床]上传：' . json_encode($data));\n        Log::write('[薄荷图床]返回：' . $result);\n\n        $json = json_decode($result, true);\n        if (isset($json['result']) && $json['result'] == 'success' && !empty($json['url'])) {\n            return $json['url'];\n        }\n\n        Log::write('[薄荷图床]上传失败', 'ERROR');\n        return false;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/Smms.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Config;\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\n\n/**\n * sm.ms图床\n * 鸣谢：[@Showfom](https://github.com/Showfom)\n * Class Smms\n * @package app\\ImageHosting\n * @url https://sm.ms\n */\nclass Smms extends ImageHosting\n{\n    public function upload($path)\n    {\n        if (empty(Config::$image_hosting_extend['smms']['token'])) {\n            Log::write('[Smms图床]上传失败：请先配置 smms 的 token', 'ERROR');\n            return false;\n        }\n\n        $data = ['smfile' => Curl::getCurlFile($path)];\n        $header = [\n            CURLOPT_HTTPHEADER => [\n                'Authorization' => Config::$image_hosting_extend['smms']['token'],\n            ],\n        ];\n        $result = Curl::post('https://sm.ms/api/v2/upload', $data, $header);\n\n        Log::write('[Smms图床]上传：' . json_encode($data));\n        Log::write('[Smms图床]返回：' . $result);\n        $result = json_decode($result, true);\n\n        if (isset($result['code'])) {\n            if ($result['code'] == 'success') {\n                return $result['data']['url'];\n            } elseif ($result['code'] == 'image_repeated') {\n                return $result['images'];\n            }\n        }\n\n        Log::write('[Smms图床]上传失败：' . $result['msg'], 'ERROR');\n        return false;\n    }\n}"
  },
  {
    "path": "app/ImageHosting/Tietuku.php",
    "content": "<?php\n\nnamespace app\\ImageHosting;\n\nuse app\\Libs\\Config;\nuse app\\Libs\\Curl;\nuse app\\Libs\\Log;\n\n/**\n * tietuku图床\n * P.S.不怎么建议使用贴图库，小毛病挺多的\n * Class Tietuku\n * @package app\\ImageHosting\n * @url http://www.tietuku.com\n */\nclass Tietuku extends ImageHosting\n{\n    public function upload($path)\n    {\n        if (empty(Config::$image_hosting_extend['tietuku']['token'])) {\n            Log::write('[Tietuku图床]上传失败：请先配置 tietuku 的 token', 'ERROR');\n            return false;\n        }\n\n        $data = [\n            'Token' => Config::$image_hosting_extend['tietuku']['token'],\n            'file'  => Curl::getCurlFile($path),\n        ];\n        $result = Curl::post('http://up.imgapi.com/', $data);\n\n        Log::write('[Tietuku图床]上传：' . json_encode($data));\n        Log::write('[Tietuku图床]返回：' . $result);\n        $result = json_decode($result, true);\n\n        if (isset($result['code'])) {\n            Log::write('[Tietuku图床]上传失败：' . $result['info'], 'ERROR');\n            return false;\n        }\n        return $result['linkurl'];\n    }\n}"
  },
  {
    "path": "app/Jobs/ClearLog.php",
    "content": "<?php\nnamespace app\\Jobs;\n\nuse app\\Libs\\Log;\nuse app\\Libs\\Storage;\n\n/**\n * 清除日志文件\n * 接受参数 -n，表示删除 n 天之前的日志。默认为 7\n */\nclass ClearLog extends Job\n{\n    public $onlyActivateByCli = true;\n\n    public function run()\n    {\n        $opt = getopt('n::');\n        $n = (!empty($opt['n']) && $opt['n'] > 0 && is_numeric($opt['n'])) ? intval($opt['n']) : 7;\n\n        $time = strtotime(\"-{$n} days\");\n        $path = STORAGE_PATH . 'logs/';\n        $deleteNum = 0;\n\n        if ($dh = opendir($path)) {\n            while (($file = readdir($dh)) !== false) {\n                if (in_array($file, ['.', '..', '.gitignore'])) {\n                    continue;\n                }\n\n                $file = $path . $file;\n                if (filemtime($file) < $time) {\n                    $deleteNum++;\n                    Storage::deleteFile($file);\n                }\n            }\n        }\n\n        Log::write(\"共计清除日志文件 {$deleteNum} 个\");\n\n        return true;\n    }\n}"
  },
  {
    "path": "app/Jobs/Job.php",
    "content": "<?php\n\nnamespace app\\Jobs;\n\nuse app\\Factory;\nuse app\\Libs\\Str;\n\n/**\n * 抽象 任务类\n * Class Job\n * @package app\\Jobs\n */\nabstract class Job extends Factory\n{\n    // 是否只能通过 cli 触发\n    public $onlyActivateByCli = false;\n\n    /**\n     * @param string $name\n     * @param array  $config\n     * @return self\n     */\n    public static function make($name, array $config = [])\n    {\n        $name = '\\\\app\\\\Jobs\\\\' . Str::studly($name);\n        return parent::make($name, $config);\n    }\n\n    /**\n     * 执行任务\n     * @return bool\n     */\n    abstract public function run();\n}"
  },
  {
    "path": "app/Jobs/Refresh.php",
    "content": "<?php\n\nnamespace app\\Jobs;\n\nuse app\\ImageHosting\\ImageHosting;\nuse app\\Libs\\Config;\nuse app\\Libs\\Lock;\nuse app\\Libs\\Pixiv;\nuse app\\Libs\\Storage;\nuse app\\Libs\\Log;\n\n/**\n * 刷新任务\n *\n * 以下 2 种情况需要刷新排行榜：\n * 1、昨天的排行榜已经出来了\n * 2、昨天的排行榜还没出，但没有 pixiv.json 文件。这可能是第一次新安装，当然需要刷新\n *\n * Class Refresh\n * @package app\\Jobs\n */\nclass Refresh extends Job\n{\n    public function run()\n    {\n        try {\n            $pixivJson = Storage::getJson('pixiv');\n            $ranking = Pixiv::getRanking();\n\n            if ($ranking === false) {\n                return false;\n            }\n\n            if(!$this->needRefresh($ranking, $pixivJson)){\n                Log::write('排行榜尚未更新，半小时后再试');\n                Lock::forceCreate('refresh', 1800);\n\n                return true;\n            }\n\n            $images = Pixiv::getImages();\n            if($images === false) {\n                throw new \\Exception('【致命错误】无法获取Pixiv排行榜图片列表');\n            }\n\n            $enableCompress = Config::$compress && function_exists('imagecreatefromjpeg');\n\n            $imageHostingInstances = [];\n            foreach (Config::$image_hosting as $ihName) {\n                $imageHostingInstances[] = ImageHosting::make($ihName);\n            }\n\n            $proxy = Config::$proxy;\n\n            // 开始获取图片\n            $pixivJson = [];\n            foreach ($images['data'] as $i => $data) {\n                // 缓存数量限制\n                if ($i >= Config::$limit) {\n                    break;\n                }\n\n                Log::write(\"开始获取第 \" . ($i + 1) . \" 张图：{$data['url']}\");\n\n                // 最多尝试下载 3 次\n                Config::$proxy = $proxy;\n                for ($ii = 1; $ii <= 3; $ii++) {\n                    $tmpfile = Pixiv::downloadImage($data['url']);\n                    if ($tmpfile && getimagesize($tmpfile)) {\n                        break;\n                    } else {\n                        Log::write(\"图片 {$data['url']} 下载失败，重试第 {$ii} 次\");\n                        sleep(mt_rand(3, 30));\n                    }\n                }\n                if (!$tmpfile) {\n                    throw new \\Exception(\"图片 {$data['url']} 下载失败\");\n                }\n\n                // 压缩图片\n                if ($enableCompress) {\n                    $image = imagecreatefromjpeg($tmpfile);\n                    if ($image) {\n                        imagejpeg($image, $tmpfile, 95);\n                        $bytes = filesize($tmpfile);\n                        Log::write('压缩后图片大小： ' . $bytes . ' 字节');\n                        imagedestroy($image);\n                        unset($image);\n                    }\n\n                    if ($bytes < 1000) {\n                        throw new \\Exception(\"图片 {$data['url']} 下载失败\");\n                    }\n                }\n\n                // 上传到图床\n                Config::$proxy = null; // 上传过程中禁用代理\n                foreach ($imageHostingInstances as $imageHosting) {\n                    $url = $imageHosting->upload($tmpfile);\n                    if ($url != false) {\n                        Storage::deleteFile($tmpfile);\n                        break;\n                    }\n                }\n\n                $url = $url ?: Pixiv::getProxyUrl($data['url']); // 如上传失败则使用反代 url\n                $data['url'] = $url;\n\n                $pixivJson['data'][] = $data;\n                $pixivJson['image'][] = $url;\n                $pixivJson['url'][] = \"artworks/{$data['id']}\";\n            }\n\n            $pixivJson['date'] = $images['date'];\n            Storage::saveJson('pixiv', $pixivJson);\n            Lock::remove('refresh');\n\n            Config::$clear_overdue && Storage::clearOverdueImages();\n\n            return true;\n\n        } catch (\\Exception $e) {\n\n            // 是否超过最大重试次数\n            $refreshCount = (int)Storage::get('refreshCount');\n            if ($refreshCount > 10) {\n                // 超过 10 次（5小时）都无法获取到 pixiv 排行榜\n                // 直接锁定一整天，明天再试，降低无意义的资源损耗\n                $expire = mktime(23, 59, 59) - time();\n                Lock::forceCreate('refresh', $expire);\n                Storage::remove('refreshCount');\n            } else {\n                // 半小时后再试\n                Lock::forceCreate('refresh', 1800);\n                Storage::save('refreshCount', $refreshCount + 1);\n            }\n\n            $this->errorMsg = $e->getMessage();\n            return false;\n        }\n    }\n\n    /**\n     * 是否需要刷新数据\n     * @param array $ranking pixiv 接口返回的排行榜数据\n     * @param array $pixivJson pixiv.json 的内容\n     * @return bool\n     * @throws \\Exception\n     */\n    private function needRefresh($ranking, $pixivJson)\n    {\n        if (!isset($pixivJson['date'])) {\n            return true;\n        }\n\n        // $ranking['date'] 的格式为 20200310\n        if ($ranking && isset($ranking['date']) && preg_match('|^\\d{8}$|', $ranking['date'])) {\n\n            return $pixivJson['date'] != date('Y-m-d', strtotime($ranking['date']));\n        }\n\n        throw new \\Exception('排行榜日期数据异常！数据：' . json_encode($ranking));\n    }\n}"
  },
  {
    "path": "app/Libs/Config.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 配置类\n * Class Config\n * @package app\\Libs\n */\nclass Config\n{\n    // 此处属性对应 config.php 内的配置项\n    public static $url = '';\n    public static $background_color = 'transparent';\n    public static $limit = 50;\n    public static $service = true;\n    public static $log_level = [];\n    public static $proxy = '';\n    public static $clear_overdue = true;\n    public static $compress = true;\n    public static $image_hosting = ['local'];\n    public static $image_hosting_extend = [];\n    public static $disable_web_job = false;\n    public static $header_script = '';\n    public static $ranking_type = '';\n\n    /**\n     * 初始化配置\n     */\n    public static function init()\n    {\n        // 读取配置项\n        $config = require BASE_PATH . 'config.php';\n        foreach ($config as $key => $value) {\n            self::$$key = $value;\n        }\n\n        // 获取本项目 url\n        if (self::$url == '' && !IS_CLI) {\n            self::$url = Request::getCurrentUrl();\n        }\n\n        // 是否对外提供服务，是则获取 url 参数\n        if (self::$service) {\n            if (isset($_GET['color'])) {\n                self::$background_color = '#' . $_GET['color'];\n            }\n            if (isset($_GET['limit'])) {\n                self::$limit = (int)$_GET['limit'];\n            }\n        }\n\n        try {\n            if (!is_writable(STORAGE_PATH)) {\n                throw new \\Exception(STORAGE_PATH . ' 目录无法写入');\n            }\n\n            if (!is_array(Config::$image_hosting) || count(Config::$image_hosting) < 1) {\n                throw new \\Exception('image_hosting 配置项至少要有一个值');\n            }\n\n            if (self::$limit < 1) {\n                throw new \\Exception('limit 配置项不得小于1');\n            }\n\n            if (IS_CLI && self::$url == '' && in_array('local', self::$image_hosting)) {\n                throw new \\Exception('在 cli 模式下使用 local 本地图床时，必须配置 url 项，否则可能会生成错误的缩略图 url');\n            }\n\n            if (!in_array(Config::$ranking_type, ['', 'illust', 'manga'])) {\n                throw new \\Exception('ranking_type 配置项必须为空、illust 或 manga');\n            }\n\n        } catch (\\Exception $e) {\n            Log::write($e->getMessage(), Log::LEVEL_ERROR);\n            echo '错误：' . $e->getMessage();\n\n            die;\n        }\n    }\n}"
  },
  {
    "path": "app/Libs/Curl.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * Curl请求类\n * Class Curl\n * @package app\\Libs\n */\nclass Curl\n{\n    /**\n     * get请求\n     * @param string $url 请求目标\n     * @param array  $opt curl参数\n     * @param bool   $includeCookie 同时返回 cookie\n     * @return bool|string|array $includeCookie 为 true 时返回 ['cookie' => '', 'html' => ''] 数组\n     */\n    public static function get($url, $opt = [], $includeCookie = false)\n    {\n        $ch = curl_init($url);\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);\n        curl_setopt($ch, CURLOPT_TIMEOUT, 30);\n        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\n        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\n        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');\n\n        Config::$proxy && curl_setopt($ch, CURLOPT_PROXY, Config::$proxy);\n        $includeCookie && curl_setopt($ch, CURLOPT_HEADER, true);\n\n        if (count($opt)) {\n            curl_setopt_array($ch, $opt);\n        }\n\n        $data = curl_exec($ch);\n        curl_close($ch);\n\n        if ($includeCookie) {\n            $data = explode(\"\\r\\n\\r\\n\", $data);\n\n            $header = array_shift($data);\n            $html = implode(\"\\r\\n\\r\\n\", $data);\n\n            preg_match_all(\"/set-cookie:([^\\r\\n]*)/i\", $header, $matches);\n\n            $cookie = '';\n            if (!empty($matches[1])) {\n                $cookieItem = [];\n                foreach ($matches[1] as $item) {\n                    $cookieItem[] = explode(';', trim($item))[0];\n                }\n                $cookie = implode('; ', $cookieItem);\n            }\n\n            return compact('cookie', 'html');\n        }\n\n        return $data;\n    }\n\n    /**\n     * post请求\n     * @param string       $url\n     * @param array|string $postData\n     * @param array        $opt\n     * @return bool|string\n     */\n    public static function post($url, $postData, $opt = [])\n    {\n        $opt[CURLOPT_POST] = true;\n        $opt[CURLOPT_POSTFIELDS] = $postData;\n\n        return self::get($url, $opt);\n    }\n\n    /**\n     * 获取curl file实例\n     * @param string $path 文件路径\n     * @return \\CURLFile|string\n     */\n    public static function getCurlFile($path)\n    {\n        return class_exists('CURLFile') ? (new \\CURLFile(realpath($path))) : ('@' . realpath($path));\n    }\n}"
  },
  {
    "path": "app/Libs/Env.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 环境变量\n * Class Env\n * @package app\\Libs\n */\nclass Env\n{\n    private static $env = [];\n\n    /**\n     * 从指定环境变量文件读取\n     * 避免被 crontab 触发时无法读到环境变量的问题\n     * @param $file\n     * @return void\n     * @throws \\Exception\n     */\n    public static function init($file = '.env')\n    {\n        if (!is_readable(BASE_PATH . $file)) {\n            throw new \\Exception('无法读取 .env 环境变量文件');\n        }\n\n        self::$env = parse_ini_file(BASE_PATH . $file);\n\n        if (self::$env === false) {\n            throw new \\Exception('无法解析 .env 环境变量文件');\n        }\n    }\n\n    /**\n     * 从环境变量中读取字符串\n     * @param string $name\n     * @param mixed $default\n     * @return mixed\n     */\n    public static function getStr($name, $default = '')\n    {\n        $data = self::$env[$name] ?: false;\n\n        return $data === false ? $default : $data;\n    }\n\n    /**\n     * 检查布尔环境变量是否存在。若不存在，返回默认值\n     * @param string $name\n     * @param mixed $default\n     * @return bool\n     */\n    public static function getBool($name, $default = false)\n    {\n        return strtolower(self::getStr($name, $default)) === 'true';\n    }\n\n    /**\n     * 检查数组环境变量是否存在。若不存在，返回默认值\n     * 数组的值为 ',' 分割的字符串\n     * @param string $name\n     * @param array $default\n     * @return array|false\n     */\n    public static function getArray($name, $default = [])\n    {\n        $data = self::getStr($name, $default);\n\n        return is_array($data) ? $data : explode(',', $data);\n    }\n}"
  },
  {
    "path": "app/Libs/Lock.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 锁\n * Class Lock\n * @package app\\Libs\n */\nclass Lock\n{\n    /**\n     * 锁是否有效\n     * @param string $name\n     * @return bool\n     */\n    public static function check($name)\n    {\n        $lock = Storage::get(\"app/{$name}.lock\");\n        if ($lock === false) {\n            return false;\n        }\n\n        return ($lock > time() || $lock == 0);\n    }\n\n    /**\n     * 创建锁\n     * @param string $name\n     * @param int    $expire 自动过期时间（秒）\n     * @return bool 创建失败或锁已存在时返回 false\n     */\n    public static function create($name, $expire = 0)\n    {\n        if (self::check($name) === false) {\n            return Storage::save(\"app/{$name}.lock\", ($expire ? (time() + $expire) : 0));\n        }\n        return false;\n    }\n\n    /**\n     * 移除锁\n     * @param string $name\n     * @return bool\n     */\n    public static function remove($name)\n    {\n        return Storage::remove(\"app/{$name}.lock\");\n    }\n\n    /**\n     * 强制创建锁\n     * @param string $name 锁名称\n     * @param int    $expire 自动过期时间（秒）\n     * @return bool\n     */\n    public static function forceCreate($name, $expire)\n    {\n        return Storage::save(\"app/{$name}.lock\", ($expire ? (time() + $expire) : 0));\n    }\n}"
  },
  {
    "path": "app/Libs/Log.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 日志类\n */\nclass Log\n{\n    const LEVEL_ERROR = 'ERROR';\n\n    /**\n     * 写日志\n     * @param string|array $message\n     * @param string       $level\n     * @return bool\n     */\n    public static function write($message, $level = 'DEBUG')\n    {\n        $level = strtoupper($level);\n\n        if (!is_array(Config::$log_level) || !in_array($level, Config::$log_level)) {\n            return false;\n        }\n\n        $message = is_array($message) ? json_encode($message) : $message;\n        $content = \"[{$level}] \" . date('Y-m-d H:i:s') . \" --> {$message}\\n\";\n        if (IS_CLI) {\n            echo $content . \"\\n\";\n        }\n\n        $file = STORAGE_PATH . 'logs/' . date('Ymd') . (IS_CLI ? '-cli' : '') . '.log';\n\n        return file_put_contents($file, $content, FILE_APPEND) !== false;\n    }\n}"
  },
  {
    "path": "app/Libs/Pixiv.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * Pixiv 类\n * Class Pixiv\n * @package app\\Libs\n */\nclass Pixiv\n{\n    /**\n     * 调用官方ajax接口获取排行榜数据\n     * @param int $page 页码，最多10页\n     * @return mixed\n     * @throws \\Exception\n     */\n    public static function getRanking($page = 1)\n    {\n        Log::write(\"正在读取排行榜第 {$page} 页\");\n\n        $params = [\n            'mode' => 'daily',\n            'p' => $page,\n            'format' => 'json',\n        ];\n        if (Config::$ranking_type) {\n            $params['content'] = Config::$ranking_type;\n        }\n\n        $response = Curl::get('https://www.pixiv.net/ranking.php?' . http_build_query($params), [\n            CURLOPT_HTTPHEADER => [\n                'Referer: https://www.pixiv.net/ranking.php?mode=daily',\n            ],\n        ]);\n        $json = json_decode($response, true);\n        if (!isset($json['contents'])) {\n            Log::write('获取排行榜数据失败！接口返回值：' . $response, Log::LEVEL_ERROR);\n            return false;\n        }\n\n        return $json;\n    }\n\n    /**\n     * 获取图片url列表\n     * @return array|false\n     * @throws \\Exception\n     */\n    public static function getImages()\n    {\n        $source = Storage::getJson('source');\n        if (is_array($source) && self::checkDate($source)) {\n            return $source;\n        }\n\n        $picNum = 0;\n        $sourceJson = [];\n        for ($page = 1; $page <= 10; $page++) {\n\n            $json = self::getRanking($page);\n            if($json === false){\n                return false;\n            }\n\n            foreach ($json['contents'] as $item) {\n                $sourceJson['data'][] = [\n                    'id' => $item['illust_id'],\n                    'url' => $item['url'],\n                    'title' => $item['title'],\n                    'tags' => $item['tags'],\n                    'width' => $item['width'],\n                    'height' => $item['height'],\n                    'page_count' => $item['illust_page_count'],\n                    'rank' => $item['rank'],\n                    'yesterday_rank' => $item['yes_rank'],\n                    'user_id' => $item['user_id'],\n                    'user_name' => $item['user_name'],\n                    'uploaded_at' => $item['illust_upload_timestamp'],\n                ];\n                // image 和 url 是为了兼容 5.x 之前的旧版本\n                $sourceJson['image'][] = $item['url'];\n                $sourceJson['url'][] = \"artworks/{$item['illust_id']}\";\n                $picNum++;\n\n                if ($picNum >= Config::$limit) {\n                    break 2;\n                }\n            }\n        }\n\n        $sourceJson['date'] = date('Y-m-d', strtotime($json['date']));\n        Storage::saveJson('source', $sourceJson);\n\n        return $sourceJson;\n    }\n\n    /**\n     * 下载 Pixiv 缩略图。成功返回临时文件名\n     * @param string $url\n     * @return string 临时文件名\n     */\n    public static function downloadImage($url)\n    {\n        // 如果 local storage 已经存有这张图（每日榜上的图片是可能存在重复的），就不再重新下载了\n        $image = Storage::getImage(pathinfo($url, PATHINFO_BASENAME));\n        $shouldCheckComplete = !$image;\n        if ($image === false) {\n            $image = Curl::get($url, [\n                CURLOPT_HTTPHEADER => [\n                    'Referer: https://www.pixiv.net/ranking.php?mode=daily',\n                ],\n            ]);\n\n            Log::write('下载到数据包大小： ' . strlen($image) . ' 字节');\n        }\n\n        if ($image) {\n            $file = explode('/', $url);\n            $file = array_pop($file);\n            $file = sys_get_temp_dir() . '/' . Str::random(16) . $file;\n\n            $bytes = file_put_contents($file, $image);\n            Log::write(\"写入文件 {$file} 大小：{$bytes} 字节\");\n\n            // 检查文件是否下载完整\n            if ($shouldCheckComplete) {\n                $response = Curl::get($url, [\n                    CURLOPT_NOBODY => true,\n                    CURLOPT_HEADER => true,\n                    CURLOPT_HTTPHEADER => [\n                        'Referer: https://www.pixiv.net/ranking.php?mode=daily',\n                    ],\n                ]);\n                $contentLength = Response::getContentLength($response);\n                if ($bytes != $contentLength) {\n                    Log::write(\"写入的文件大小与目标 content-length: {$contentLength} 不符\");\n                    return false;\n                }\n            }\n\n            return $bytes > 0 ? $file : false;\n        }\n\n        return false;\n    }\n\n    /**\n     * 检查传入数组的 date 值是否有效（即大于等于昨天）。返回 true 为未过期\n     * @param array $data\n     * @return bool\n     */\n    public static function checkDate(array $data)\n    {\n        if(isset($data['date'])){\n            $yesterday = date('Y-m-d', strtotime('-1 day'));\n\n            return $data['date'] >= $yesterday;\n        }\n\n        return false;\n    }\n\n    /**\n     * 获取基于 pixiv.cat 提供的代理服务的图片 url\n     * 可以直接展示在页面上，突破 pixiv 的反盗链\n     * @param $url\n     * @return array|string|string[]\n     */\n    public static function getProxyUrl($url)\n    {\n        return str_replace('i.pximg.net', 'i.pixiv.re', $url);\n    }\n}"
  },
  {
    "path": "app/Libs/Request.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 请求类\n */\nclass Request\n{\n    /**\n     * 获取当前请求的协议\n     * @return string\n     */\n    public static function getScheme()\n    {\n        if (isset($_SERVER['REQUEST_SCHEME'])) {\n            return $_SERVER['REQUEST_SCHEME'];\n        }\n\n        if (\n            (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') ||\n            (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||\n            (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off')\n        ) {\n            return 'https';\n        }\n\n        return 'http';\n    }\n\n    /**\n     * 获取当前 url （以 / 结尾。不含 query 参数及文件名）\n     * @return string\n     */\n    public static function getCurrentUrl()\n    {\n        $url = self::getScheme() . '://' . $_SERVER['HTTP_HOST'];\n        if (!in_array($_SERVER['SERVER_PORT'], ['80', '443'])) {\n            $url .= ':' . $_SERVER['SERVER_PORT'];\n        }\n\n        $scriptName = explode('/', $_SERVER['SCRIPT_NAME']);\n        $scriptName = array_pop($scriptName);\n\n        $path = explode($scriptName, $_SERVER['REQUEST_URI']);\n        $path = array_shift($path);\n\n        $url .= rtrim($path, '/') . '/';\n\n        return $url;\n    }\n\n    /**\n     * 执行刷新线程\n     */\n    public static function execRefreshThread()\n    {\n        if (Config::$disable_web_job) {\n            return;\n        }\n\n        Config::$proxy = null;\n        Curl::get(Config::$url . 'index.php?job=refresh', [\n            CURLOPT_TIMEOUT => 1,\n        ]);\n    }\n}"
  },
  {
    "path": "app/Libs/Response.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\nclass Response\n{\n    /**\n     * 从一段 http 响应中获取 content-length\n     * @param string $response\n     * @return false|int\n     */\n    public static function getContentLength($response)\n    {\n        $response = explode(\"\\n\", $response);\n        foreach ($response as $line) {\n            $line = trim($line);\n\n            if (stripos($line, 'Content-Length: ') === 0) {\n                $line = explode(': ', $line);\n                if (!empty($line[1]) && is_numeric($line[1])) {\n                    return (int)$line[1];\n                }\n\n                break;\n            }\n        }\n\n        return false;\n    }\n}"
  },
  {
    "path": "app/Libs/Storage.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 存储类\n * Class Storage\n * @package app\\Libs\n */\nclass Storage\n{\n    /**\n     * 保存到文件\n     * @param string       $file\n     * @param string|array $content\n     * @return bool\n     */\n    public static function save($file, $content)\n    {\n        $file = STORAGE_PATH . $file;\n        return file_put_contents($file, $content) !== false;\n    }\n\n    /**\n     * 获取文件内容\n     * @param string $file\n     * @return mixed|false\n     */\n    public static function get($file)\n    {\n        $file = STORAGE_PATH . $file;\n        if (is_readable($file) === false) {\n            return false;\n        }\n        $content = @file_get_contents($file);\n        if ($content === false) {\n            Log::write(\"读取 {$file} 文件失败\");\n            return false;\n        }\n\n        return $content;\n    }\n\n    /**\n     * 删除文件\n     * @param string $file 文件名\n     * @return bool\n     */\n    public static function remove($file)\n    {\n        return self::deleteFile(STORAGE_PATH . $file);\n    }\n\n    /**\n     * 清除过期的图片\n     */\n    public static function clearOverdueImages()\n    {\n        $deleteNum = 0;\n        $time = strtotime(date('Ymd')); // 获取今日0点的时间戳，早于此时间戳的文件都得死\n        if ($dh = opendir(STORAGE_PATH . 'images/')) {\n            while (($file = readdir($dh)) !== false) {\n                if (in_array($file, ['.', '..', '.gitignore'])) {\n                    continue;\n                }\n                $file = STORAGE_PATH . 'images/' . $file;\n                if (filemtime($file) < $time) {\n                    $deleteNum++;\n                    self::deleteFile($file);\n                }\n            }\n        }\n        Log::write(\"共计清除过期图片 {$deleteNum} 张\");\n    }\n\n    /**\n     * 获取图片内容。文件不存在或无效时返回 false\n     * @param string $name\n     * @return mixed|false\n     */\n    public static function getImage($name)\n    {\n        $path = 'images/' . $name;\n        if (file_exists(STORAGE_PATH . $path) && getimagesize(STORAGE_PATH . $path)) {\n            return self::get($path);\n        }\n        return false;\n    }\n\n    /**\n     * 删除文件\n     * @param string $path\n     * @return bool\n     */\n    public static function deleteFile($path)\n    {\n        return @unlink($path);\n    }\n\n    /**\n     * 保存数组到json文件\n     * @param string $file 文件名。无需后缀名\n     * @param array  $data\n     * @return bool\n     */\n    public static function saveJson($file, array $data)\n    {\n        return self::save(\"app/{$file}.json\", json_encode($data));\n    }\n\n    /**\n     * 获取json数组内容\n     * @param string $file 文件名。无需后缀名\n     * @return mixed|false\n     */\n    public static function getJson($file)\n    {\n        $content = self::get(\"app/{$file}.json\");\n        $content = json_decode($content, true);\n        if (!is_array($content)) {\n            return false;\n        }\n\n        return $content;\n    }\n}"
  },
  {
    "path": "app/Libs/Str.php",
    "content": "<?php\n\nnamespace app\\Libs;\n\n/**\n * 字符串类\n */\nclass Str\n{\n    /**\n     * key-value 转为 KeyValue\n     * @param $value\n     * @return string\n     */\n    public static function studly($value)\n    {\n        $words = explode(' ', str_replace(['-', '_'], ' ', $value));\n\n        $studlyWords = array_map(function ($word) {\n            return ucfirst($word);\n        }, $words);\n\n        return implode($studlyWords);\n    }\n\n    /**\n     * 生成指定数量的随机字符串\n     * @param int $length 长度\n     * @param string $chars 从这些字符中随机选择。默认为 0123456789abcdefghijklmnopqrstuvwxyz\n     * @return string\n     */\n    public static function random($length, $chars = '0123456789abcdefghijklmnopqrstuvwxyz')\n    {\n        $charsLength = strlen($chars);\n        $randomString = '';\n        for ($i = 0; $i < $length; $i++) {\n            $randomString .= $chars[rand(0, $charsLength - 1)];\n        }\n\n        return $randomString;\n    }\n}"
  },
  {
    "path": "app/Views/index.php",
    "content": "<!-- 来自 mokeyjay 的 Pixiv每日排行榜小挂件 -->\n<!-- 博客：https://www.mokeyjay.com -->\n<!-- 这个博客将会集技术、ACG、日常、分享于一身。如果你喜欢，常来玩哦 -->\n\n<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n  <meta name=\"referrer\" content=\"no-referrer\">\n  <title>Pixiv 每日排行榜 Top<?= app\\Libs\\Config::$limit ?> 小挂件</title>\n\n  <style>\n    body { background-color : <?= app\\Libs\\Config::$background_color ?>; }\n    * { margin: 0; padding: 0; outline: none; }\n\n    .carousel {\n      overflow: hidden;\n    }\n    .list {\n      height: 100vh;\n      position: relative;\n    }\n    .list-item {\n      position: absolute;\n      width: 100%;\n      display: flex;\n      align-items: center;\n      height: 100%;\n      visibility: hidden;\n    }\n    img {\n      width: 100vw;\n    }\n\n    .list-item.current {\n      z-index: 2;\n      visibility: visible;\n    }\n\n    /* 翻页动画 */\n    .current-to-prev, .current-to-next, .next-to-current, .prev-to-current {\n      z-index: 1;\n      visibility: visible;\n    }\n    .current-to-prev {\n      animation: slide-current-to-prev .5s cubic-bezier(0.34, 0.69, 0.1, 1);\n    }\n    @keyframes slide-current-to-prev {\n      from { transform: translateX(0) }\n      to { transform: translateX(-100vw) }\n    }\n    .current-to-next {\n      animation: slide-current-to-next .5s cubic-bezier(0.34, 0.69, 0.1, 1);\n    }\n    @keyframes slide-current-to-next {\n      from { transform: translateX(0) }\n      to { transform: translateX(100vw) }\n    }\n    .next-to-current {\n      animation: slide-next-to-current .5s cubic-bezier(0.34, 0.69, 0.1, 1);\n    }\n    @keyframes slide-next-to-current {\n      from { transform: translateX(100vw) }\n      to { transform: translateX(0) }\n    }\n    .prev-to-current {\n      animation: slide-prev-to-current .5s cubic-bezier(0.34, 0.69, 0.1, 1);\n    }\n    @keyframes slide-prev-to-current {\n      from { transform: translateX(-100vw) }\n      to { transform: translateX(0) }\n    }\n\n    /* 左右翻页按钮 */\n    .button {\n      height: 100%;\n      width: 50px;\n      position: fixed;\n      top: 0;\n      z-index: 5;\n      cursor: pointer;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      transition: transform .3s ease-in-out, opacity .5s ease-in-out;\n      opacity: 0;\n    }\n    .button.prev {\n      left: 0;\n      transform: translateX(-70px);\n    }\n    .button.next {\n      right: 0;\n      transform: translateX(70px);\n    }\n    .button .arrow {\n      transform: rotate(45deg);\n      margin-top: -16px;\n    }\n    .button i {\n      position: relative;\n    }\n    .button.prev i:before, .button.prev i:after, .button.next i:before, .button.next i:after {\n      display: block;\n      content: \"\";\n      border: 4px solid white;\n      border-radius: 2px;\n      position: absolute;\n      width: 16px;\n      height: 16px;\n      box-sizing: border-box;\n    }\n    .button.prev i:before, .button.prev i:after {\n      border-top: none;\n      border-right: none;\n    }\n    .button.next i:before, .button.next i:after {\n      border-bottom: none;\n      border-left: none;\n    }\n    .button i:before {\n      filter: blur(4px);\n      border-color: #777777 !important;\n    }\n    .carousel:hover .button {\n      transform: translateX(0);\n      opacity: 1;\n    }\n\n    .mask {\n      height: 150px;\n      width: 100%;\n      position: fixed;\n      bottom: 0;\n      background: linear-gradient(to top, rgba(0, 0, 0, .3), transparent);\n      transform: translateY(150px);\n      transition: transform .3s ease-in-out\n    }\n    .carousel:hover .mask {\n      transform: translateY(0);\n    }\n\n    /* 作品 & 作者信息 */\n    .info {\n      opacity: 0;\n      text-shadow: black 0.1em 0.1em 0.2em;\n      pointer-events: none;\n      bottom: 60px;\n      position: fixed;\n      text-align: center;\n      color: white;\n      width: 80%;\n      transition: opacity .3s ease-in-out;\n      transform: translateX(10%);\n    }\n    .carousel:hover .info { opacity: 1 }\n    .title, .author {\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n    .title {\n      font-size: 1rem;\n    }\n    .author {\n      margin-top: 5px;\n      font-size: .75rem;\n    }\n\n  </style>\n  <script>\n      <?= app\\Libs\\Config::$header_script ?>\n  </script>\n</head>\n<body>\n  <div class=\"carousel\">\n    <div class=\"list\">\n      <?php foreach ($pixivJson['data'] as $k => $data): ?>\n        <div class=\"list-item <?= $k === 0 ? 'current' : '' ?>\">\n          <a href=\"https://www.pixiv.net/artworks/<?= $data['id'] ?>\" target=\"_blank\">\n            <img src=\"<?= $k === 0 ? $data['url'] : '' ?>\" data-src=\"<?= $data['url'] ?>\" alt=\"<?= htmlentities($data['title']) ?>\">\n            <div class=\"mask\"></div>\n            <div class=\"info\">\n              <div class=\"title\"><?= htmlentities($data['title']) ?></div>\n              <div class=\"author\"><?= htmlentities($data['user_name']) ?></div>\n            </div>\n          </a>\n        </div>\n      <?php endforeach; ?>\n    </div>\n    <div class=\"control\">\n      <div class=\"button prev\" onclick=\"carousel.prev()\">\n        <div class=\"arrow\"><i></i></div>\n      </div>\n      <div class=\"button next\" onclick=\"carousel.next()\">\n        <div class=\"arrow\"><i></i></div>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    const $ = document.querySelector.bind(document);\n    const $$ = document.querySelectorAll.bind(document);\n\n    class Carousel {\n      // 自动播放句柄\n      autoPlayInterval = 0;\n      // 滑动手势开始位置\n      startX = 0;\n\n      constructor($dom) {\n        // 轮播组件\n        this.$carousel = $dom\n        // 轮播列表\n        this.$carouselList = this.$carousel.querySelector('.list')\n        // 当前展示项\n        this.$currentItem = this.$carouselList.querySelector('.list-item.current')\n      }\n\n      init() {\n        this.loadImage(\n          this.findNextItemByDirection(this.$currentItem, 'prev'),\n          this.findNextItemByDirection(this.$currentItem, 'next')\n        )\n\n        this.registerAutoPlay()\n        this.registerMouseHoverPausePlay()\n        this.registerSlideGesture()\n      }\n\n      // 根据翻页方向获取下一个项目\n      findNextItemByDirection($item, direction) {\n        let prop = direction === 'next' ? 'next' : 'previous'\n        let nextItem = $item[`${prop}ElementSibling`]\n\n        // 对应方向找不到下一个 item，就说明滑动到尽头了，要跳到开头或结尾\n        if (nextItem === null) {\n          prop = direction === 'next' ? 'first' : 'last'\n          nextItem = this.$carouselList.querySelector(`.list-item:${prop}-child`)\n        }\n\n        return nextItem\n      }\n\n      // 翻页\n      switchPage(direction) {\n        const $nextItem = this.findNextItemByDirection(this.$currentItem, direction);\n\n        [this.$currentItem, $nextItem].map($item => {\n          $item.addEventListener('animationend', function end() {\n            if ($item.classList.contains('next-to-current') || $item.classList.contains('prev-to-current')) {\n              $item.className = 'list-item current'\n            } else {\n              $item.className = 'list-item'\n            }\n\n            $item.removeEventListener('animationend', end)\n          })\n        })\n\n        if (direction === 'next') {\n          this.$currentItem.className = 'list-item current-to-prev'\n          $nextItem.className = 'list-item next-to-current'\n        } else {\n          this.$currentItem.className = 'list-item current-to-next'\n          $nextItem.className = 'list-item prev-to-current'\n        }\n\n        // 预加载下一张图\n        this.loadImage(this.findNextItemByDirection($nextItem, direction))\n\n        this.$currentItem = $nextItem\n      }\n\n      // 加载图片\n      loadImage() {\n        for (let $item of arguments) {\n          let $img = $item.querySelector('img')\n          if ($img.getAttribute('src') === '') {\n            $img.setAttribute('src', $img.getAttribute('data-src'))\n          }\n        }\n      }\n\n      next() {\n        this.switchPage('next')\n      }\n      prev() {\n        this.switchPage('prev')\n      }\n\n      // 注册自动播放\n      registerAutoPlay() {\n        clearInterval(this.autoPlayInterval)\n        this.autoPlayInterval = setInterval(() => this.next(), 5000)\n      }\n\n      // 注册鼠标移入暂停轮播\n      registerMouseHoverPausePlay() {\n        this.$carousel.addEventListener('pointerenter', () => clearInterval(this.autoPlayInterval))\n        this.$carousel.addEventListener('pointerleave', () => this.registerAutoPlay())\n      }\n\n      // 注册左右滑动手势\n      registerSlideGesture() {\n        this.$carousel.addEventListener('touchstart', e => this.startX = e.changedTouches[0].pageX);\n        this.$carousel.addEventListener('touchend', e => {\n          let endX = e.changedTouches[0].pageX;\n          let diffX = endX - this.startX;\n\n          if (diffX > 50) {\n            this.prev()\n          } else if (diffX < -50) {\n            this.next()\n          }\n        });\n      }\n    }\n\n    const carousel = new Carousel($('.carousel'));\n    carousel.init()\n\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "app/Views/loading.php",
    "content": "<?php\nuse app\\Libs\\Config;\n?>\n<!-- 来自 mokeyjay 的 Pixiv每日排行榜小挂件 -->\n<!-- 博客：https://www.mokeyjay.com -->\n<!-- 这个博客将会集技术、ACG、日常、分享于一身，如果你喜欢，常来玩哦 -->\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Pixiv 每日排行榜 Top<?=Config::$limit?> 小挂件</title>\n  <style>\n    html, body { height : 100%; background-color : <?=Config::$background_color?>; }\n    body { display: flex; align-items: center; justify-content: center; margin: 0; }\n    div { text-align : center; }\n\n    @keyframes spinner-two-alt {\n      0% {transform: rotate(0deg)}\n      to {transform: rotate(359deg)}\n    }\n\n    .gg-spinner-two-alt,.gg-spinner-two-alt::before {\n      box-sizing: border-box;\n      display: block;\n      width: 40px;\n      height: 40px;\n      color: #aaaaaa;\n    }\n\n    .gg-spinner-two-alt {\n      position: relative;\n      margin: 20px auto;\n    }\n\n    .gg-spinner-two-alt::before {\n      content: \"\";\n      position: absolute;\n      border-radius: 100px;\n      animation: spinner-two-alt 1s cubic-bezier(.6,0,.4,1) infinite;\n      border: 6px solid transparent;\n      border-bottom-color: currentColor;\n      border-top-color: currentColor\n    }\n\n    .tip { font-size: 22px; color: #aaaaaa; }\n  </style>\n  <script>\n    setTimeout(function() {\n      location.reload()\n    }, 30000)\n  </script>\n</head>\n<body>\n<div>\n  <i class=\"gg-spinner-two-alt\"></i>\n  <div class=\"tip\">排行榜更新中<br>请稍候</div>\n</div>\n</body>\n<script>\n  window.onload = function(){\n    if (navigator.language.substring(0, 2).toLowerCase() !== 'zh') {\n      document.querySelector('div.tip').innerHTML = 'Ranking Updating<br>Just a moment'\n    }\n  }\n</script>\n</html>"
  },
  {
    "path": "app/autoload.php",
    "content": "<?php\n\nspl_autoload_register(function ($class_name) {\n    $class_name = str_replace('\\\\', '/', $class_name);\n    require BASE_PATH . $class_name . '.php';\n});"
  },
  {
    "path": "config.php",
    "content": "<?php\n\nreturn [\n    /**\n     * 本项目的url地址，必须以 / 结尾\n     * 留空则自动获取，一般情况下留空即可\n     *\n     * P.S. 如果你准备通过 cli 方式来触发 refresh 任务，且使用了 local 图床，则此项必填\n     *      否则生成的图片完整 url 可能出现问题\n     *\n     * The url address of this project, must end with /\n     * Leave it blank to get it automatically, normally leave it blank\n     *\n     * P.S. If you want to trigger the refresh job via cli, and you are using a local image-hosting, this field is required\n     *      Otherwise, the generated image full URL may have problems\n     */\n    'url' => '',\n\n    /**\n     * 背景颜色。默认值为 transparent （透明）。你也可以通过 url 参数 color 来设置\n     * Background color. The default value is transparent. You can also set the background color by url parameter 'color'\n     */\n    'background_color' => 'transparent',\n\n    /**\n     * 显示和缓存的图片最大数量（范围1-500）\n     * 例如将此值设为 10 则可以做出 Top10 的效果\n     * 也可防止部分辣鸡主机在缓存图片时占用过多资源导致卡死或报警\n     * 一般情况下默认的 50 就行\n     *\n     * Maximum number of images to display and cache (range 1-500)\n     * For example, if you set this value to 10, you can make a Top10 effect\n     * It also prevents some low-performance hosts from using too many resources when caching images, which can lead to jamming or alarms\n     * Usually the default 50 is fine\n     */\n    'limit' => 50,\n\n    /**\n     * 是否对外提供服务\n     * 为 true 时，任何人都可通过 url 的 get 参数来临时修改 background_color 和 limit 的值\n     *\n     * Whether to provide external services\n     * When true, anyone can temporarily change the values of background_color and limit by the url parameter\n     */\n    'service' => true,\n\n    /**\n     * 日志级别。可多选：DEBUG、ERROR 或留空不记录任何日志\n     * Logging level. Multiple options: DEBUG, ERROR or leave blank to not record any logs\n     */\n    'log_level' => [],\n\n    /**\n     * 代理服务器配置。例如 127.0.0.1:1080\n     * 留空为不使用代理\n     *\n     * Proxy server configuration. For example 127.0.0.1:1080\n     * Leave blank to not use proxy\n     */\n    'proxy' => '',\n\n    /**\n     * 每次更新排行榜数据后，自动删除过期的本地缓存缩略图\n     * Automatically delete expired local cache thumbnails after each ranking data update\n     */\n    'clear_overdue' => true,\n\n    /**\n     * 压缩缩略图，在几乎不损失画质的前提下减小 50% 左右的体积，降低服务器带宽压力\n     * 需要启用 PHP 的 GD 扩展\n     *\n     * Compress thumbnails to reduce the size by about 50% with almost no loss of image quality, reducing server bandwidth pressure\n     * Need the GD extension for PHP\n     */\n    'compress' => true,\n\n    /**\n     * 图床名称\n     * （推荐度按照顺序从高到低）\n     *\n     * 推荐填写多个图床，如果其中一个图床上传失败，则将按照顺序继续尝试其他图床\n     *\n     * Image-Hosting\n     * (Recommendation is ranked from highest to lowest)\n     *\n     * 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\n     */\n    'image_hosting' => ['fifty-eight', 'chkaja', 'catbox', 'local'],\n\n    /**\n     * 图床扩展配置信息\n     * Extend Configuration information for the image-hosting\n     */\n    'image_hosting_extend' => [\n        'tietuku' => [\n            'token' => ''\n        ],\n        'smms' => [\n            'token' => '',\n        ],\n        // 薄荷图床\n        'riyugo' => [\n            // 客服提供的会员专属网址。例如 https://r789.com/1234，必须以 / 结尾\n            'url' => '',\n            // 要上传到的文件夹。通常可以留空\n            'upload_path' => '',\n            // 管理后台-设置 中的 唯一用户ID\n            'unique_id' => '',\n            // 登录管理后台后，filemanagerXXXXXXXXX 这个 cookie 的值\n            // （XXXXXXXX 是你的唯一用户 ID）\n            'token' => '',\n        ],\n    ],\n\n    /**\n     * 禁用 web 访问的方式触发 job 更新，仅限 cli 方式触发\n     * 由于部分环境 web 超时时间不够，会导致更新操作不断被触发但又无法完成整个更新流程\n     * 因此添加一个开关，避免 web 触发更新，节约服务器资源\n     *\n     * Disable web-trigger update job, cli-trigger only\n     * See doc/deploy.en.md\n     */\n    'disable_web_job' => false,\n\n    /**\n     * 放置在页面 <header> 标签下的 js 脚本内容，通常用来放置统计代码\n     * 无需 <script> 标签\n     *\n     * Js script content placed under the <header> tag of the page, usually used to place statistical code\n     * Doesn't need <script> tag\n     */\n    'header_script' => '',\n\n    /**\n     * 排行榜类型。支持 综合、插画、漫画 三种类型\n     * 留空为综合； illust 为插画； manga 为漫画\n     *\n     * The type of ranking.\n     * Support three types: synthesis, illustration and manga.\n     * Leave '' for synthesis, 'illust' for illustration, 'manga' for manga\n     */\n    'ranking_type' => '',\n];"
  },
  {
    "path": "demo.html",
    "content": "<!doctype html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>Pixiv 每日排行榜小挂件</title>\n</head>\n<body>\n  <div style=\"margin-top: 200px; text-align: center;\">\n    <iframe src=\"index.php\" style=\"width:240px; height:380px; border: 0;\"></iframe>\n    <br>\n    <br>\n    鼠标移动到挂件上可控制图片滚动、查看作品信息；点击图片可访问作品详情页\n    <br>\n    Mouse hover the image to control the image scrolling, view the work information; click the image to visit the work details page\n  </div>\n</body>\n</html>"
  },
  {
    "path": "doc/advance-usage.en.md",
    "content": "# Advanced usage\n```html\n<iframe src=\"https://pixiv.mokeyjay.com\" style=\"width:240px;  height:380px;  border: 0\"></iframe>\n```\n\n## Custom width or height\nThis widget supports adaptive width and height. You only need to change the value of `width` or `height` in the code\nFor example, if you want a width of 300px and height of 500px , then change the code above from  \n`width:240px;  height:380px;` \n\nto `width:300px; height:500px;`\n\n> Since most pixiv images are previewed at a resolution of about 240*380, there is usually no need to modify the width and height.\n\n## Custom background color\nThe default background color is transparent. If you want to change the background color, you can add a parameter `color` to the URL.\nFor example, if you want a red background, then change the above code from  \n`/pixiv` \n\nto `/pixiv?color=f00`\n\n`f00` means red, supports 3- or 6-digit [HTML Color Codes](https://htmlcolorcodes.com/), **without the # sign**\n\n## Customize the returned pic number\nBy default, only the top 50 entries are displayed. You can change this with the URL parameter `limit`.\n> the limit has a maximum of 500 since the maximum order of Pixiv Ranking is 500.\n\nFor example, if you only want the first 10, then change the above code from  \n`/pixiv` to `/pixiv?limit=10`\n\n> ⚠️ This value cannot exceed the limit configuration in `config.php`\n\n> 🌟 To use more than one URL argument at a time, you can connect them with `&`. Such as `?color=f00&limit=10`"
  },
  {
    "path": "doc/advance-usage.md",
    "content": "# 高级用法\n```html\n<iframe src=\"https://pixiv.mokeyjay.com\" style=\"width:240px; height:380px; border: 0\"></iframe>\n```\n\n## 自定义宽度或高度\n此挂件支持自适应宽高，你只需要修改代码中的 `width` 或者 `height` 的值即可  \n例如你想要宽 300、高 500，则需要将上述代码中的  \n`width:240px; height:380px;` 修改为 `width:300px; height:500px;`  \n\n> 因为大多数 pixiv 画作的预览图都是 240*380 左右的分辨率，因此一般并不需要修改宽高度\n\n## 自定义背景色\n默认背景色为透明，如需修改背景色，可以添加 url 参数 `color`  \n例如你想要红色背景，则将上述代码中的  \n`/pixiv` 修改为 `/pixiv?color=f00`  \n\n其中 `f00` 即表示红色，支持 3 或 6 位 [十六进制颜色值](https://baike.baidu.com/item/%E5%8D%81%E5%85%AD%E8%BF%9B%E5%88%B6%E9%A2%9C%E8%89%B2%E7%A0%81/10894232) ，无需 # 号\n\n## 自定义数量\n默认只显示排行前 50 的作品。你可以通过 url 参数 `limit` 来修改  \n> limit 最大值为 500，因为排行榜最多就只有 500 名\n\n例如你只想要前 10，则将上述代码中的  \n`/pixiv` 修改为 `/pixiv?limit=10`\n\n> ⚠️ 该值无法超过 `config.php` 中的 limit 配置\n\n> 🌟 如需同时使用多个 url 参数，你可以用 `&` 来连接它们。例如 `?color=f00&limit=10`"
  },
  {
    "path": "doc/deploy.en.md",
    "content": "# Deployment\n## Docker\n```shell\ndocker run -d -p 80:80 --name=pixiv -e URL=http://localhost/ ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n```\nSee [Docker](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/docker.en.md)\n## Local\n- [Download source code](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/releases/latest)\n- Unzip under your the web directory\n- Edit `config.php` with a **professional** editor (e.g. `Visual Studio Code`, `Sublime`, etc., notepad is not allowed) and modify the configuration according to your own needs\n- Grant write permission to `storage` directory\n\n## Trigger updates proactively\nBy default, this widget will check if the ranking data is out of date every time it is accessed and make an additional request to trigger an automatic update if needed. In most cases, this operation is not required.\n\nHowever, in some cases (e.g. PHP timeouts are not long enough, server performance is too poor), this web-triggered update will be interrupted before it finishes. This will interrupt the update and never let it finish, and also keeps consuming unnecessary performance and bandwidth.  \nIf you encounter the problem above, you can turn off web-triggered updates (set `disable_web_job` to `false` in `config.php`)  \nThen trigger update proactively via cli, e.g. `php index.php -j=refresh`\n\n> You can use a tool like `crontab` to automatically trigger updates on a regular basis. For example `*/30 * * * * php /path/to/pixiv/index.php -j=refresh`  \n> It means run update every 30 minutes. The program will automatically determine whether it really needs to be updated, and it won't waste performance\n\n## Clear the logs\nOpening the log (set `log_level` to `['DEBUG', 'ERROR']` in `config.php`) will log this widget's running info.  \nWhen giving me feedback on a problem, it is usually recommended to send me the logs and configuration file as a zip package (please do not post the configuration file to the public)  \nIf you are going to keep the log enabled for a long time and are worried about it taking up too much hard drive, you can delete the logs over 7 days with `php index.php -j=clear-log`\n\n> Add the parameter `-n=3` to delete logs that are 3 days old\n\n> You can use a tool like `crontab` to delete automatically on a regular basis. For example `30 1 * * * php /path/to/pixiv/index.php -j=clear-log`"
  },
  {
    "path": "doc/deploy.md",
    "content": "# 部署\n## Docker\n```shell\ndocker run -d -p 80:80 --name=pixiv -e URL=http://localhost/ ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n```\n详见 [Docker](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/docker.md)\n## 本地部署\n- [下载源代码](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/releases/latest)\n- 解压缩到 web 目录下\n- 使用专业编辑器（例如 `Visual Studio Code`、`Sublime` 等，禁止使用记事本）编辑 `config.php`，根据实际情况修改相应配置\n> 由于 Pixiv 已经被墙，如果你想要将此项目部署在中国大陆境内，可能需要配置 `proxy` 配置项\n\n- 给予 `storage` 目录写入权限\n\n## 主动触发更新\n默认情况下，此挂件每次被访问都会检查排行榜数据是否已过期，并在需要时额外发起一次请求来触发自动更新。通常不需要干预  \n\n但在有些情况下（例如 PHP 超时时间设的不够长、服务器性能较低），这种通过 web 方式触发的更新还没执行完就被中断，不仅导致更新始终无法完成，还会一直浪费性能及带宽  \n如果你遇到同样的问题，可以关闭 web 方式触发更新（将 `config.php` 中的 `disable_web_job` 设为 `false`）  \n然后通过 cli 方式主动触发，例如 `php index.php -j=refresh`  \n\n> 你可以使用 `crontab` 之类的工具来定时自动触发更新。例如 `*/30 * * * * php /path/to/pixiv/index.php -j=refresh`  \n> 表示每 30 分钟执行一次更新。程序会自动判断是否真的需要更新，不会浪费性能\n\n## 清除日志\n打开日志（`config.php` 中的 `log_level` 设为 `['DEBUG', 'ERROR']`）可以将此挂件的运行信息记录下来  \n向我反馈问题时，通常也建议将日志和配置文件打包发送给我（请勿将配置文件发布到公开场合）  \n如果你准备长期打开日志又担心它占用过多的硬盘，可以通过 `php index.php -j=clear-log` 来删除 7 天前的日志  \n> 添加参数 `-n=3` 即删除 3 天前的日志\n\n> 你可以使用 `crontab` 之类的工具来定时自动删除。例如 `30 1 * * * php /path/to/pixiv/index.php -j=clear-log`"
  },
  {
    "path": "doc/docker.en.md",
    "content": "# Docker\n> If you need to enable multiple containers, share the `/var/www/html/storage` directory between them by mounting it to avoid each container updating the ranking data separately and causing performance waste\n\n## Deployment\n### Command\n```shell\ndocker run -d -p 80:80 --name=pixiv -e URL=http://localhost/ ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n```\n\n### Docker compose\n```yaml\nversion: '3.1'\n\nservices:\n  pixiv:\n    image: ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n    container_name: pixiv\n    restart: always\n    environment:\n      URL: http://localhost/\n    ports:\n      - \"80:80\"\n```\n\n> `URL` is the access url to the container, supports path, and must end with `/`\n\n## Configure\nBy [environment](https://docs.docker.com/compose/compose-file/#environment) . all config items see [config.docker.php](../.docker/config.php)\n\n> Only the `local` image hosting is enabled by default (images are stored locally to the container). To use it, you must configure the `URL` item  \n> \n> Different from local deployment, Docker image has built-in scheduling tasks that automatically update ranking data. Therefore, the default value of `DISABLE_WEB_JOB` is `true`, which means that updates are not triggered through web access\n\n> logs path: `/var/www/html/storage/logs`\n\n## Jobs\n### Trigger updates proactively\n> In general, ranking data is detected for updates every half hour without being actively triggered  \n> When you deploy for the first time, you can trigger an update manually or wait for half an hour to update automatically\n\n```shell\ndocker exec pixiv php index.php -j=refresh\n```\nSee [Trigger updates proactively](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.en.md)\n\n### Clear the logs\n```shell\ndocker exec pixiv php index.php -j=clear-log\n```\nSee [Clear the logs](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.en.md)"
  },
  {
    "path": "doc/docker.md",
    "content": "# Docker\n> 如需启用多个容器，请通过挂载目录的方式在它们之间共享 `/var/www/html/storage` 目录，避免每个容器各自更新排行榜数据，造成性能浪费\n\n## 部署\n### 命令行\n```shell\ndocker run -d -p 80:80 --name=pixiv -e URL=http://localhost/ ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n```\n\n### Docker compose\n```yaml\nversion: '3.1'\n\nservices:\n  pixiv:\n    image: ghcr.io/mokeyjay/pixiv-daily-ranking-widget\n    container_name: pixiv\n    restart: always\n    environment:\n      URL: http://localhost/\n    ports:\n      - \"80:80\"\n```\n\n> `URL` 是指向这个容器的访问地址，支持路径，必须以 `/` 结尾\n\n## 配置\n通过 [环境变量](https://docs.docker.com/compose/compose-file/#environment) 进行配置。所有配置项见 [config.docker.php](../.docker/config.php)\n\n> 默认只启用了 `local` 图床（即图片存储在容器本地）。使用它时，必须配置 `URL` 项  \n> \n> 与本地部署不同，Docker 镜像内置了自动更新排行榜数据的定时任务，因此 `DISABLE_WEB_JOB` 默认为 `true`，即不通过 web 访问触发更新\n\n> 日志路径：`/var/www/html/storage/logs`\n\n## 任务\n### 主动触发更新\n> 通常情况下，排行榜数据会每半小时检测一次更新，无需主动触发  \n> 首次部署时，可以手动触发一次更新，或等待半小时自动更新\n\n```shell\ndocker exec pixiv php index.php -j=refresh\n```\n详见 [主动触发更新](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.md)\n\n### 清除日志\n```shell\ndocker exec pixiv php index.php -j=clear-log\n```\n详见 [清除日志](https://github.com/mokeyjay/Pixiv-daily-ranking-widget/blob/master/doc/deploy.md)"
  },
  {
    "path": "doc/log.en.md",
    "content": "# History Update Log\n## 5.2\n### New Features\n- Support Docker (thanks to @hujingnb)\n- `header_script` configuration item, so you can customize statistics code or js script\n### Optimizations\n- Used the proxy service provided by pixiv.cat as a final guarantee plan\n- Riyugo image hosting changed to vip version, you need to buy a vip account before use it (free version is no longer available)\n- Improve image download integrity check mechanism\n- Removed built-in statistics code\n### Other\n- Removed invalid JD, imgurl, imgtg and saoren image hosting\n\n## 5.1\n### New Features\n- JD, Riyugo, FiftyEight image hosting\n- `static_cdn` configuration, you can choose the front-end static resource CDN provider\n### Optimizations\n- Rewritten left and right arrows, fixed some issues with it\n### Other\n- Removed the invalid Baidu, Imgstop image hosting option\n- Replace Google Analytics with Baidu Analytics\n\n## 5.0\n### New Features\n- Supporting cross-domain on the APIs\n- Scheduled job to clear historical logs\n- Supporting 8 free image hosting\n### Optimizations\n- Picture display effect\n- Left and right arrows will now be hidden automatically\n- Display arwork title and author on mouse hover\n- Upgrade front-end dependency packages to their latest while reducing amount of dependencies\n- Improved logging function\n- Enrich interface data to fit more use cases\n- Replace Baidu Analytics with Google Analytics\n- Update default User Agent\n### Fixes\n- Project URL cannot be retrieved correctly under certain scenarios\n- A blank image will be downloaded under certain scenarios\n### Other\n- Removed the invalid Alibaba image hosting option\n\n## 4.4.2\n- Update default UA of Curl class\n- Remove invalid image host\n- Update Alibaba image host\n- Add `disable_web_job` switch, see `config.php` for details\n\n## 4.4.1\n- Update default UA of Curl class\n- Fix an error of getting wrong project URL in some cases\n- Fix the retry loop when fails to get ranking data.\n- Optimize the logic to determine whether the data needs to be updated\n- Delete the obsolete Alibaba image host\n\n## 4.4\n- Use official ajax interface to get ranking data\n- Add Alibaba, Baidu, toutiao.com image host API\n- Update smms image host API to v2 version\n- Remove deprecated img.sb image host API\n- Remove unaccessible Jingdong image host API\n- Remove statistics code on loading page\n- Use comprehensive ranking data instead of just illustrations rank\n- Expanded maximum number of returned images to 500, corresponding to the official limit\n- The value of  `service` configuration item no longer affects the limit.\n- Other optimizations and bug fixes\n\n## 4.3\n- Fix failed to update the Pixiv ranking page due to pixiv change.\n- Use https when jumping to detail page\n\n## 4.2\n- Fix failed to update the Pixiv ranking page due to pixiv change.\n- Add a maximum number of retries if failing to get the ranking data.\n\n## 4.1\n- Support transparent background\n\n## 4.0\n- New version with almost all the codes rewritten, more new features and bugs for you to discover!\n- Since pixiv has enabled global anti-hotlink, the `download` and `url_cache` options have been removed. Now it will force download thumbnails, and then upload them to the corresponding image host or store them locally on the server according to the configuration\n\n> Mumbling: I initially stated this project just for fun, and never expected the code to get so ugly as features piled up. It's so shameful that this project became my most starred project on GitHub. Luckily I spent some days to deal with the 4.0 version, and it feels better now.\n>\n> BTW this little thing now supports multiple image host, it will save hundreds of gigs of traffic every month now yingyingying\n\n## 3.0\n- Add `$download_proxy` configuration option\n- Since Pixiv's has enabled anti-hotlink and images cannot be displayed directly, `$download` is enabled by default.\n  For better display, self-deployed users are recommended to configure a timed task to trigger `download.php` at 0:00 every day\n\n## 2.9\n- Fix the problem of not functioning normally due to Pixiv changes\n\n## 2.8\n- Try to optimize update lock to prevent repeated updates under high concurrency\n- Separate the configuration item `Conf::$url_cache` from `Conf::$download`, now you can only cache the image url instead of the thumbnail url\n- Add support for tietuku.cn image host\n\n> The free version of tietuku.cn is not very good and does not support https, I suggest using sm.ms first and tietuku.cn only as a fallback option.\n> Due to some problems with the previous update lock not working well under high concurrency, my server IP was blocked by sm.ms image host due to duplicate uploads. And I personally can't afford to support the high CDN cost. <del>So effective immediately **Plan One** no longer provides official CDN acceleration and instead gets images directly from the Pixiv site</del  \n> Plan 1 is currently supported by 360 Site Defender CDN\n\n## 2.7\n- Add image compression feature to reduce server bandwidth stress (requires GD library)\n- Fix sm.ms image host support to reduce chances to fail\n- Add sm.ms image host upload log\n\n> If there is a problem with opening `$enable_smms`, please paste the log file when giving feedback\n\n## 2.6\n- Add sm.ms image host support. With one click to enable, it can significantly reduce the server bandwidth stress and save traffic. Thanks to [@Showfom](https://sb.sb/) for providing the image host\n\n> I'm not going to tell you that I added this feature because it hurts that the several gigabytes of traffic will be consumed every day on Plan 1\n> If the upload fails 3 times repeatedly, image will be read locally from the server to ensure normal access\n\n## 2.5\n- Fix the problem of not functioning normally due to Pixiv changes\n- Pixiv supports native https now! Hooray!\n\n## 2.4\n- Fix the problem that the `limit` parameter of URL is invalid under certain circumstances\n- Repair the caching problem of **Plan 1**\n- Fix the SSL certificate problem\n\n## 2.3\n- Replace the reference url of front-end library, fix the problem of slow loading with China Mobile as ISP\n- Add adaptive protocol feature, fix the problem of the small green lock being affected when the cache is disabled or the cache peogress is not finished\n- The above update comes from the friendly PR of @LingWuLuKong, let's PRPR her together to show our appreciation!\n- The Plan One from mokeyJay a service now supports HTTPS. The CDN charge is high, please use and cherish!\n- If it is abused to the point that I can not afford the cost, then the service may be suspended~\n- If the number of visits is high, it is recommended to build your own services, thank you for your support and understanding!\n\n## 2.2\n- Optimize download threads to support self-deployed HTTPS\n\n## 2.1\n- I must be drunk when planning the 2.0 roadmap to constrain all logic into one file. Although the overall performance has been improved, the same problems still occur in some cases. For example, the thumbnail will fail to download, PHP will timeout so the download will break, and so on. So when I tested it and realized this, I started working on a new version <del>Facepalm</del>.\n- Remove the auto update lock mechanism, the thumbnails will no longer be re-downloaded when they already exist and are valid. Prevents thumbnail download failures due to network fluctuations or timeouts\n\n## 2.0\n- Overall refactoring, overall performance is greatly optimized\n- Add an automatic update lock mechanism to avoid wasting resources on concurrent updates under high concurrency scenario\n- New pseudo multi-thread automatic update mechanism, background update does not affect widget use\n- Update failure retrial, to avoid the failure of retrieving some images due to network problems\n\n## Motivations\nI was talking to a friend the other day, and he said he wanted to show the daily ranking of [Pixiv](http://www.pixiv.net/) in the sidebar of his blog. I am also an `ACG` lover myself, so I wanted to write this as well. I finally had time last night and spent more than half an hour writing it. It looks great on [my own blog](https://www.mokeyjay.com), so I polished it and added some features and made it open source in case you have the same requirements."
  },
  {
    "path": "doc/log.md",
    "content": "# 历史更新日志\n## 5.2\n### 新增\n- 支持 Docker 部署（感谢 @hujingnb ）\n- `header_script` 配置项，以便自行部署的用户自定义访问统计或其他 js 代码\n### 优化\n- 使用了 pixiv.cat 提供的代理服务作为最后的保底方案\n- 薄荷图床改为会员版本，需要购买会员后才能使用（免费版已失效）\n- 完善图片下载完整度检查机制\n- 去掉了内置的访问统计代码\n### 其他\n- 去掉失效的京东、imgurl、imgtg、saoren 图床\n\n## 5.1\n### 新增\n- 京东、薄荷、58图床\n- `static_cdn` 配置项，可以选择前端静态资源 CDN 供应商\n### 优化\n- 重写了左右翻页箭头，修复了它的一些问题\n### 其他\n- 去掉失效的百度、映画图床\n- 将谷歌分析更换为百度统计\n\n## 5.0\n### 新增\n- 支持跨域请求的数据接口\n- clear-log 任务用于清除历史日志文件\n- 共计 8 个国内外公开、免费的图床\n### 优化\n- 图片显示效果\n- 左右翻页箭头现在会自动隐藏了\n- 鼠标悬浮时显示作品标题及作者名称\n- 升级前端依赖包至最新、减少依赖\n- 完善日志记录功能\n- 丰富接口数据以适应更多场景\n- 将百度统计更换为谷歌统计\n- 更新默认 UA\n### 修复\n- 部分情况下无法正确获取项目 url 的问题\n- 部分情况下可能会下载到空白图片的问题\n### 其他\n- 去掉失效的阿里巴巴图床\n\n## 4.4.2\n- 更新 Curl 类的默认 UA\n- 删除已经失效的图床\n- 更新阿里巴巴图床\n- 添加 `disable_web_job` 开关，详见 `config.php`\n\n## 4.4.1\n- 更新 Curl 类的默认 UA\n- 修复部分情况下获取项目URL错误\n- 修复获取排行榜数据失败时会无限重试的问题\n- 优化是否需要更新数据的判断机制\n- 删除已经失效的 alibaba 图床\n\n## 4.4\n- 改用官方 ajax 接口获取排行数据\n- 添加 阿里巴巴、百度、今日头条 图床接口\n- 更新 smms 图床接口到 v2 版本\n- 删除已被废弃的 img.sb 图床接口\n- 删除已被封锁的 京东 图床接口\n- 删除 loading 页面的统计代码\n- 改用综合排行榜数据，而非仅限于插画\n- 图片数量限制扩充到 500，达到官方上限\n- service 配置项的取值不再影响 limit\n- 其他优化、bug 修复\n\n## 4.3\n- 修复Pixiv排行榜页面代码改版导致的无法更新\n- 跳转至详情页时使用 https\n\n## 4.2\n- 修复Pixiv排行榜页面代码改版导致的无法更新\n- 添加获取排行榜失败时的最大重试次数\n\n## 4.1\n- 现已支持透明背景\n\n## 4.0\n- 几乎重写了所有代码的船新版本，更多新特性与bug等你来发掘！\n- 由于pixiv全面开启反盗链，为了迎合此变化。已将`download`和`url_cache`这两个不再有存在意义的开关去除。现在会强制下载缩略图，然后再根据配置上传到各个图床或存储在服务器本地\n> 碎碎念：原本这个项目只是随便搞搞，没想到后面功能越堆越多，代码也越来越丑。作为本辣鸡github上最高star的项目实在是丢人。好在我花了几天时间撸了这个4.0，总算是不那么丢人了  \n> 还有就是添加了多图床的支持，每个月能节省几百G的流量了嘤\n\n## 3.0\n- 添加`$download_proxy`配置项\n- 由于Pixiv的图片url添加了防盗链无法被直接显示，因此`$download`配置项默认开启\n  为了更好的显示效果，自行部署的用户建议配置一个定时任务，每天0点触发`download.php`\n\n## 2.9\n- 修复因Pixiv改动导致挂掉的问题\n\n## 2.8\n- 尝试优化更新锁，防止高并发下重复更新\n- 从 Conf::$download 中独立出配置项 Conf::$url_cache，现在可以仅缓存图片url而不缓存缩略图了\n- 添加贴图库图床支持\n\n> 贴图库免费版并不是很好用且不支持https，建议优先使用sm.ms，贴图库仅作为备用\n> 由于之前更新锁在高并发下有些问题无法很好的发挥作用，导致我的服务器IP因重复上传被sm.ms图床封了。而我个人也无力支撑高昂的CDN费用。<del>因此即日起**方案一**不再提供CDN加速，改为直接从P站获取图片</del>  \n> 方案一目前由360网站卫士提供CDN支持\n\n## 2.7\n- 添加图片压缩功能，降低服务器带宽压力（需要GD库）\n- 修复sm.ms图床支持，降低失败概率\n- 添加sm.ms图床上传日志\n\n> 如果开启`$enable_smms`出现问题，反馈时请带上日志文件\n\n## 2.6\n- 添加sm.ms图床支持。一键启用即可大幅降低服务器带宽压力、节省流量。感谢[@Showfom](https://sb.sb/)提供图床\n\n> 我才不告诉你是因为方案一每天跑掉我几G流量，心疼不已才加的这个功能呢\n> 如果连续3次上传失败，则从服务器本地读取图片，确保访问正常\n\n## 2.5\n- 修复因Pixiv改版导致挂掉的问题\n- Pixiv原生支持https啦！可喜可贺\n\n## 2.4\n- 修复特定情况下URL的`limit`参数无效的问题\n- 修复**方案一**缓存问题\n- 修复上面效果图SSL证书问题\n\n## 2.3\n- 更换了前端库引用地址，修复移动宽带下加载慢的问题\n- 添加协议自适应，修复在关闭缓存或缓存还没全部完成时影响小绿锁的问题\n- 以上更新来自@灵乌路空 的友情PR，我们一起对她PRPR以示感激吧\n- 超能小紫的方案一服务现已支持HTTPS。咬牙忍痛上了收费CDN，请大家且用且珍惜\n- 要是被滥用到我吃不消费用的话可能会暂停服务噢~\n- 如果访问量较高的话建议还是自行搭建服务，谢谢各位的支持与谅解\n\n## 2.2\n- 优化下载线程以支持自行部署HTTPS\n\n## 2.1\n- 规划2.0时脑子抽了，非要把所有逻辑都局限在一个文件里。虽然各方面确实有所提升，但在一些情况下照样会出现那些老问题。例如缩略图下载失败啊、PHP超时导致下载中断之类。因此在我测试并意识到这一点时，赶紧开始了新版本的开发 <del>光速打脸</del>\n- 去除自动更新锁机制，缩略图已存在并且有效时不再重复下载。防止因网络波动或超时导致的缩略图下载失败\n\n## 2.0\n- 整体重构，各机制大幅优化\n- 添加自动更新锁机制，避免高访问量时并发更新浪费资源\n- 全新的伪多线程自动更新机制，后台更新不影响使用\n- 更新失败重试，避免因为网络问题导致的部分图片获取失败\n\n# 初衷\n前几天跟朋友聊天，朋友说希望能在自己博客侧边栏中显示[Pixiv](http://www.pixiv.net/)的每日排行榜。我自己也是个`ACG`爱好者，被他这么一说也想弄一个。昨晚终于有空，花了半个多小时写完。[自己博客](https://www.mokeyjay.com)用上了感觉不错，完善了一下加了点功能开源出来福利各位"
  },
  {
    "path": "index.php",
    "content": "<?php\n/**\n * 项目：Pixiv 每日排行榜小挂件\n * 版本：6.0\n * 作者：mokeyjay\n * 博客：https://www.mokeyjay.com\n * 源码：https://github.com/mokeyjay/Pixiv-daily-ranking-widget\n * 可随意修改、二次发布。但请保留上方版权声明及注明出处\n */\n\ndefine('BASE_PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR);\ndefine('APP_PATH', BASE_PATH . 'app' . DIRECTORY_SEPARATOR);\ndefine('STORAGE_PATH', BASE_PATH . 'storage' . DIRECTORY_SEPARATOR);\ndefine('IS_CLI', PHP_SAPI === 'cli');\n\nrequire APP_PATH . 'autoload.php';\n\napp\\App::run();"
  },
  {
    "path": "storage/app/.gitignore",
    "content": "*\n!.gitignore"
  },
  {
    "path": "storage/images/.gitignore",
    "content": "*\n!.gitignore"
  },
  {
    "path": "storage/logs/.gitignore",
    "content": "*\n!.gitignore"
  }
]