diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..0384844 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..d098423 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,30 @@ +--- +name: 问题模板 +about: 如发现Bug,请按此模板提交issues,不按模板提交的问题将直接关闭。 +提交问题务必描述清楚、附上日志,描述不清导致无法理解和分析的问题也可能会被直接关闭。 +--- + +## 你使用的 NAStool 是什么版本,什么环境? + +> NAStool 版本: vx.x.x +> +> 环境: docker or windows or Synology +> + +## 你遇到什么问题了? + +> 描述一下你遇到的问题 + +## 是否已经浏览过Issues、Wiki及TG公众号仍无法解决? + +> 请搜索Issues列表、查看wiki跟TG公众号的更新说明,已经解释过的问题不要重复提问 + + +## 你期望的结果 + +> 描述以下你期望的结果 + +## 给出程序界面截图、后台运行日志或配置文件 + +> 如UI BUG请提供截图及配置文件截图 +> 其它问题提供后台日志,如为Docker请提供docker的日志 diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..7c68cd7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,18 @@ +--- +name: 功能需求模板 +about: 如有新功能需要需要提交,请按此模板创建issues +--- + +## 你使用的 NAStool 是什么版本,什么环境? + +> NAStool 版本: vx.x.x +> +> 环境: docker or windows or synology + +## 你想要新增或者改进什么功能? + +> 你想要新增或者改进什么功能? + +## 这个功能有什么可以参考的资料吗? + +> 这个功能有什么可以参考的资料吗?是否可以列举一些,不要引用同类但商业化软件的任何内容. diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..103bd5f --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,54 @@ +name: Build NAStool Beta Image +on: + workflow_dispatch: + push: + branches: + - dev + paths: + - version.py + - docker/Dockerfile.beta + - .github/workflows/build-beta.yml + - requirements.txt +jobs: + build: + runs-on: ubuntu-latest + name: Build Docker Image + steps: + - + name: Checkout + uses: actions/checkout@master + + - + name: Release version + id: release_version + run: | + app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") + echo "app_version=$app_version" >> $GITHUB_ENV + + - + name: Set Up QEMU + uses: docker/setup-qemu-action@v1 + + - + name: Set Up Buildx + uses: docker/setup-buildx-action@v1 + + - + name: Login DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - + name: Buildx + uses: docker/build-push-action@v2 + with: + context: . + file: docker/Dockerfile.beta + platforms: | + linux/amd64 + linux/arm64 + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }}-beta \ No newline at end of file diff --git a/.github/workflows/build-lite.yml b/.github/workflows/build-lite.yml new file mode 100644 index 0000000..5c8c0b7 --- /dev/null +++ b/.github/workflows/build-lite.yml @@ -0,0 +1,54 @@ +name: Build NAStool Lite Image +on: + workflow_dispatch: + push: + branches: + - master + paths: + - version.py + - docker/Dockerfile.lite + - .github/workflows/build-lite.yml + - requirements.txt +jobs: + build: + runs-on: ubuntu-latest + name: Build Docker Image + steps: + - + name: Checkout + uses: actions/checkout@master + + - + name: Release version + id: release_version + run: | + app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") + echo "app_version=$app_version" >> $GITHUB_ENV + + - + name: Set Up QEMU + uses: docker/setup-qemu-action@v1 + + - + name: Set Up Buildx + uses: docker/setup-buildx-action@v1 + + - + name: Login DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - + name: Build Lite Image + uses: docker/build-push-action@v2 + with: + context: . + file: docker/Dockerfile.lite + platforms: | + linux/amd64 + linux/arm64 + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }}-lite \ No newline at end of file diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..1bb737d --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,98 @@ +name: Build NAStool Windows +on: + workflow_dispatch: + push: + branches: + - master + paths: + - version.py + - .github/workflows/build-windows.yml + - windows/** + +jobs: + Windows-build: + runs-on: windows-latest + steps: + - name: init Python 3.10.6 + uses: actions/setup-python@v4 + with: + python-version: '3.10.6' + - name: install dependent packages + run: | + python -m pip install --upgrade pip + pip install wheel numpy==1.23.5 pyparsing==3.0.9 wxpython==4.2.0 pyinstaller==5.7.0 + git clone --depth=1 -b master https://github.com/NAStool/nas-tools --recurse-submodule + cd nas-tools + pip install -r requirements.txt + echo ("NASTOOL_CONFIG=D:/a/nas-tools/nas-tools/nas-tools/config/config.yaml") >> $env:GITHUB_ENV + echo $env:NASTOOL_CONFIG + shell: pwsh + - name: package through pyinstaller + run: | + cd nas-tools + copy .\windows\rely\upx.exe c:\hostedtoolcache\windows\python\3.10.6\x64\Scripts + copy .\windows\rely\hook-cn2an.py c:\hostedtoolcache\windows\python\3.10.6\x64\lib\site-packages\pyinstaller\hooks + copy .\windows\rely\hook-zhconv.py c:\hostedtoolcache\windows\python\3.10.6\x64\lib\site-packages\pyinstaller\hooks + copy .\third_party.txt .\windows + copy .\windows\rely\template.jinja2 c:\hostedtoolcache\windows\Python\3.10.6\x64\lib\site-packages\setuptools\_vendor\pyparsing\diagram + xcopy .\web c:\hostedtoolcache\windows\python\3.10.6\x64\lib\site-packages\web\ /e + xcopy .\config c:\hostedtoolcache\windows\python\3.10.6\x64\lib\site-packages\config\ /e + xcopy .\db_scripts c:\hostedtoolcache\windows\python\3.10.6\x64\lib\site-packages\db_scripts\ /e + cd windows + pyinstaller nas-tools.spec + shell: pwsh + - name: upload windows file + uses: actions/upload-artifact@v3 + with: + name: windows + path: D:/a/nas-tools/nas-tools/nas-tools/windows/dist/nas-tools.exe + + Create-release_Send-message: + runs-on: ubuntu-latest + needs: [Windows-build] + steps: + - uses: actions/checkout@v2 + - name: Release version + id: release_version + run: | + app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") + echo "app_version=$app_version" >> $GITHUB_ENV + - name: download exe and rename + uses: actions/download-artifact@v3 + - name: get release_informations + shell: bash + run: | + pwd + mkdir releases + cd windows + mv nas-tools.exe /home/runner/work/nas-tools/nas-tools/releases/nastool_win_v${{ env.app_version }}.exe + pwd + - name: Create release + id: create_release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ env.app_version }} + release_name: v${{ env.app_version }} + body: ${{ github.event.commits[0].message }} + draft: false + prerelease: false + - name: Upload release asset + uses: dwenegar/upload-release-assets@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + release_id: ${{ steps.create_release.outputs.id }} + assets_path: | + /home/runner/work/nas-tools/nas-tools/releases/ + - name: Send telegram message (release informations) + uses: appleboy/telegram-action@master + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + format: markdown + message: | + *v${{ env.app_version }}* + + ${{ github.event.commits[0].message }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5cc9a27 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,55 @@ +name: Build NAStool Image +on: + workflow_dispatch: + push: + branches: + - master + paths: + - version.py + - docker/Dockerfile + - docker/Dockerfile.lite + - .github/workflows/build.yml + - requirements.txt +jobs: + build: + runs-on: ubuntu-latest + name: Build Docker Image + steps: + - + name: Checkout + uses: actions/checkout@master + + - + name: Release version + id: release_version + run: | + app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") + echo "app_version=$app_version" >> $GITHUB_ENV + + - + name: Set Up QEMU + uses: docker/setup-qemu-action@v1 + + - + name: Set Up Buildx + uses: docker/setup-buildx-action@v1 + + - + name: Login DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Image + uses: docker/build-push-action@v2 + with: + context: . + file: docker/Dockerfile + platforms: | + linux/amd64 + linux/arm64 + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/nas-tools:latest + ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72644b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__ +*.sock +*.log +*.pid +test.py + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +gen/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c4b7027 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "third_party/qbittorrent-api"] + path = third_party/qbittorrent-api + url = https://github.com/rmartin16/qbittorrent-api +[submodule "third_party/transmission-rpc"] + path = third_party/transmission-rpc + url = https://github.com/Trim21/transmission-rpc +[submodule "third_party/anitopy"] + path = third_party/anitopy + url = https://github.com/igorcmoura/anitopy +[submodule "third_party/plexapi"] + path = third_party/plexapi + url = https://github.com/pkkid/python-plexapi +[submodule "third_party/slack_bolt"] + path = third_party/slack_bolt + url = https://github.com/slackapi/bolt-python +[submodule "third_party/feapder"] + path = third_party/feapder + url = https://github.com/jxxghp/feapder diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e8f6fe --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +![logo-blue](https://user-images.githubusercontent.com/51039935/197520391-f35db354-6071-4c12-86ea-fc450f04bc85.png) +# NAS媒体库管理工具 + +[![GitHub stars](https://img.shields.io/github/stars/NAStool/nas-tools?style=plastic)](https://github.com/NAStool/nas-tools/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/NAStool/nas-tools?style=plastic)](https://github.com/NAStool/nas-tools/network/members) +[![GitHub issues](https://img.shields.io/github/issues/NAStool/nas-tools?style=plastic)](https://github.com/NAStool/nas-tools/issues) +[![GitHub license](https://img.shields.io/github/license/NAStool/nas-tools?style=plastic)](https://github.com/NAStool/nas-tools/blob/master/LICENSE.md) +[![Docker pulls](https://img.shields.io/docker/pulls/jxxghp/nas-tools?style=plastic)](https://hub.docker.com/r/jxxghp/nas-tools) +[![Platform](https://img.shields.io/badge/platform-amd64/arm64-pink?style=plastic)](https://hub.docker.com/r/jxxghp/nas-tools) + + +Docker:https://hub.docker.com/repository/docker/jxxghp/nas-tools + +TG频道:https://t.me/nastool + +API: http://localhost:3000/api/v1/ + + +## 功能: + +NAS媒体库管理工具。 + + +## 安装 +### 1、Docker +``` +docker pull jxxghp/nas-tools:latest +``` +教程见 [这里](docker/readme.md) 。 + +如无法连接Github,注意不要开启自动更新开关(NASTOOL_AUTO_UPDATE=false),将NASTOOL_CN_UPDATE设置为true可使用国内源加速安装依赖。 + +### 2、本地运行 +python3.10版本,需要预安装cython,如发现缺少依赖包需额外安装 +``` +git clone -b master https://github.com/NAStool/nas-tools --recurse-submodule +python3 -m pip install -r requirements.txt +export NASTOOL_CONFIG="/xxx/config/config.yaml" +nohup python3 run.py & +``` + +### 3、Windows +下载exe文件,双击运行即可,会自动生成配置文件目录 + +https://github.com/NAStool/nas-tools/releases + +### 4、群晖套件 +添加矿神群晖SPK套件源直接安装: + +https://spk.imnks.com/ + +https://spk7.imnks.com/ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/brushtask.py b/app/brushtask.py new file mode 100644 index 0000000..68ef906 --- /dev/null +++ b/app/brushtask.py @@ -0,0 +1,861 @@ +import re +import sys +import time +from datetime import datetime + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler + +import log +from app.downloader.client import Qbittorrent, Transmission +from app.filter import Filter +from app.helper import DbHelper +from app.message import Message +from app.rss import Rss +from app.sites import Sites +from app.utils import StringUtils, Torrent, ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import BrushDeleteType +from config import BRUSH_REMOVE_TORRENTS_INTERVAL, Config + + +@singleton +class BrushTask(object): + message = None + sites = None + filter = None + dbhelper = None + _scheduler = None + _brush_tasks = [] + _torrents_cache = [] + _downloader_infos = [] + _qb_client = "qbittorrent" + _tr_client = "transmission" + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.message = Message() + self.sites = Sites() + self.filter = Filter() + # 移除现有任务 + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + # 读取下载器列表 + downloaders = self.dbhelper.get_user_downloaders() + self._downloader_infos = [] + for downloader_info in downloaders: + self._downloader_infos.append( + { + "id": downloader_info.ID, + "name": downloader_info.NAME, + "type": downloader_info.TYPE, + "host": downloader_info.HOST, + "port": downloader_info.PORT, + "username": downloader_info.USERNAME, + "password": downloader_info.PASSWORD, + "save_dir": downloader_info.SAVE_DIR + } + ) + # 读取刷流任务列表 + self._brush_tasks = self.get_brushtask_info() + if not self._brush_tasks: + return + # 启动RSS任务 + task_flag = False + self._scheduler = BackgroundScheduler(timezone=Config().get_timezone()) + for task in self._brush_tasks: + if task.get("state") == "Y" and task.get("interval") and str(task.get("interval")).isdigit(): + task_flag = True + self._scheduler.add_job(func=self.check_task_rss, + args=[task.get("id")], + trigger='interval', + seconds=int(task.get("interval")) * 60) + # 启动删种任务 + if task_flag: + self._scheduler.add_job(func=self.remove_tasks_torrents, + trigger='interval', + seconds=BRUSH_REMOVE_TORRENTS_INTERVAL) + # 启动 + self._scheduler.print_jobs() + self._scheduler.start() + log.info("刷流服务启动") + + def get_brushtask_info(self, taskid=None): + """ + 读取刷流任务列表 + """ + brushtasks = self.dbhelper.get_brushtasks() + _brush_tasks = [] + for task in brushtasks: + site_info = self.sites.get_sites(siteid=task.SITE) + if site_info: + site_url = StringUtils.get_base_url(site_info.get("signurl") or site_info.get("rssurl")) + else: + site_url = "" + downloader_info = self.get_downloader_info(task.DOWNLOADER) + _brush_tasks.append({ + "id": task.ID, + "name": task.NAME, + "site": site_info.get("name"), + "site_id": task.SITE, + "interval": task.INTEVAL, + "state": task.STATE, + "downloader": task.DOWNLOADER, + "downloader_name": downloader_info.get("name"), + "transfer": task.TRANSFER, + "free": task.FREELEECH, + "rss_rule": eval(task.RSS_RULE), + "remove_rule": eval(task.REMOVE_RULE), + "seed_size": task.SEED_SIZE, + "rss_url": site_info.get("rssurl"), + "cookie": site_info.get("cookie"), + "sendmessage": task.SENDMESSAGE, + "forceupload": task.FORCEUPLOAD, + "ua": site_info.get("ua"), + "download_count": task.DOWNLOAD_COUNT, + "remove_count": task.REMOVE_COUNT, + "download_size": StringUtils.str_filesize(task.DOWNLOAD_SIZE), + "upload_size": StringUtils.str_filesize(task.UPLOAD_SIZE), + "lst_mod_date": task.LST_MOD_DATE, + "site_url": site_url + }) + if taskid: + for task in _brush_tasks: + if task.get("id") == int(taskid): + return task + return {} + else: + return _brush_tasks + + def check_task_rss(self, taskid): + """ + 检查RSS并添加下载,由定时服务调用 + :param taskid: 刷流任务的ID + """ + if not taskid: + return + # 任务信息 + taskinfo = self.get_brushtask_info(taskid) + if not taskinfo: + return + # 任务属性 + seed_size = taskinfo.get("seed_size") + task_name = taskinfo.get("name") + site_id = taskinfo.get("site_id") + rss_url = taskinfo.get("rss_url") + rss_rule = taskinfo.get("rss_rule") + cookie = taskinfo.get("cookie") + rss_free = taskinfo.get("free") + ua = taskinfo.get("ua") + # 查询站点信息 + site_info = self.sites.get_sites(siteid=site_id) + if not site_info: + log.error("【Brush】刷流任务 %s 的站点已不存在,无法刷流!" % task_name) + return + site_name = site_info.get("name") + site_proxy = site_info.get("proxy") + + if not rss_url: + log.error("【Brush】站点 %s 未配置RSS订阅地址,无法刷流!" % site_name) + return + if rss_free and not cookie: + log.warn("【Brush】站点 %s 未配置Cookie,无法开启促销刷流" % site_name) + return + # 下载器参数 + downloader_cfg = self.get_downloader_info(taskinfo.get("downloader")) + if not downloader_cfg: + log.error("【Brush】任务 %s 下载器不存在,无法刷流!" % task_name) + return + + log.info("【Brush】开始站点 %s 的刷流任务:%s..." % (site_name, task_name)) + # 检查是否达到保种体积 + if not self.__is_allow_new_torrent(taskid=taskid, + taskname=task_name, + seedsize=seed_size, + downloadercfg=downloader_cfg, + dlcount=rss_rule.get("dlcount")): + return + + rss_result = Rss.parse_rssxml(rss_url) + if len(rss_result) == 0: + log.warn("【Brush】%s RSS未下载到数据" % site_name) + return + else: + log.info("【Brush】%s RSS获取数据:%s" % (site_name, len(rss_result))) + + # 同时下载数 + max_dlcount = rss_rule.get("dlcount") + success_count = 0 + new_torrent_count = 0 + if max_dlcount: + downloading_count = self.__get_downloading_count(downloader_cfg) or 0 + new_torrent_count = int(max_dlcount) - int(downloading_count) + + for res in rss_result: + try: + # 种子名 + torrent_name = res.get('title') + # 种子链接 + enclosure = res.get('enclosure') + # 种子页面 + page_url = res.get('link') + # 种子大小 + size = res.get('size') + # 发布时间 + pubdate = res.get('pubdate') + + if enclosure not in self._torrents_cache: + self._torrents_cache.append(enclosure) + else: + log.debug("【Brush】%s 已处理过" % torrent_name) + continue + + # 检查种子是否符合选种规则 + if not self.__check_rss_rule(rss_rule=rss_rule, + title=torrent_name, + torrent_url=page_url, + torrent_size=size, + pubdate=pubdate, + cookie=cookie, + ua=ua, + proxy=site_proxy): + continue + # 开始下载 + log.debug("【Brush】%s 符合条件,开始下载..." % torrent_name) + if self.__download_torrent(downloadercfg=downloader_cfg, + title=torrent_name, + enclosure=enclosure, + size=size, + taskid=taskid, + transfer=True if taskinfo.get("transfer") == 'Y' else False, + sendmessage=True if taskinfo.get("sendmessage") == 'Y' else False, + forceupload=True if taskinfo.get("forceupload") == 'Y' else False, + upspeed=rss_rule.get("upspeed"), + downspeed=rss_rule.get("downspeed"), + taskname=task_name, + site_info=site_info): + # 计数 + success_count += 1 + # 添加种子后不能超过最大下载数量 + if max_dlcount and success_count >= new_torrent_count: + break + + # 再判断一次 + if not self.__is_allow_new_torrent(taskid=taskid, + taskname=task_name, + seedsize=seed_size, + dlcount=rss_rule.get("dlcount"), + downloadercfg=downloader_cfg): + break + except Exception as err: + ExceptionUtils.exception_traceback(err) + continue + log.info("【Brush】任务 %s 本次添加了 %s 个下载" % (task_name, success_count)) + + def remove_tasks_torrents(self): + """ + 根据条件检查所有任务下载完成的种子,按条件进行删除,并更新任务数据 + 由定时服务调用 + """ + + def __send_message(_task_name, _delete_type, _torrent_name): + """ + 发送删种消息 + """ + _msg_title = "【刷流任务 {} 删除做种】".format(_task_name) + _msg_text = "删除原因:{}\n种子名称:{}".format(_delete_type.value, _torrent_name) + self.message.send_brushtask_remove_message(title=_msg_title, text=_msg_text) + + # 遍历所有任务 + for taskinfo in self._brush_tasks: + if taskinfo.get("state") != "Y": + continue + try: + # 总上传量 + total_uploaded = 0 + # 总下载量 + total_downloaded = 0 + # 可以删种的种子 + delete_ids = [] + # 需要更新状态的种子 + update_torrents = [] + # 任务信息 + taskid = taskinfo.get("id") + task_name = taskinfo.get("name") + download_id = taskinfo.get("downloader") + remove_rule = taskinfo.get("remove_rule") + sendmessage = True if taskinfo.get("sendmessage") == "Y" else False + + # 当前任务种子详情 + task_torrents = self.dbhelper.get_brushtask_torrents(taskid) + torrent_ids = [item.DOWNLOAD_ID for item in task_torrents if item.DOWNLOAD_ID] + if not torrent_ids: + continue + # 下载器参数 + downloader_cfg = self.get_downloader_info(download_id) + if not downloader_cfg: + log.warn("【Brush】任务 %s 下载器不存在" % task_name) + continue + # 下载器类型 + client_type = downloader_cfg.get("type") + # qbittorrent + if client_type == self._qb_client: + downloader = Qbittorrent(config=downloader_cfg) + # 检查完成状态的 + torrents, has_err = downloader.get_torrents(ids=torrent_ids, status=["completed"]) + # 看看是否有错误, 有错误的话就不处理了 + if has_err: + log.warn("【BRUSH】任务 %s 获取种子状态失败" % task_name) + continue + remove_torrent_ids = list( + set(torrent_ids).difference(set([torrent.get("hash") for torrent in torrents]))) + for torrent in torrents: + # ID + torrent_id = torrent.get("hash") + # 已开始时间 秒 + dltime = int(time.time() - torrent.get("added_on")) + # 已做种时间 秒 + date_done = torrent.completion_on if torrent.completion_on > 0 else torrent.added_on + date_now = int(time.mktime(datetime.now().timetuple())) + seeding_time = date_now - date_done if date_done else 0 + # 分享率 + ratio = torrent.get("ratio") or 0 + # 上传量 + uploaded = torrent.get("uploaded") or 0 + total_uploaded += uploaded + # 平均上传速度 Byte/s + avg_upspeed = int(uploaded / dltime) + # 已未活动 秒 + last_activity = int(torrent.get("last_activity", 0)) + iatime = date_now - last_activity if last_activity else 0 + # 下载量 + downloaded = torrent.get("downloaded") + total_downloaded += downloaded + need_delete, delete_type = self.__check_remove_rule(remove_rule=remove_rule, + seeding_time=seeding_time, + ratio=ratio, + uploaded=uploaded, + avg_upspeed=avg_upspeed, + iatime=iatime) + if need_delete: + log.info( + "【Brush】%s 做种达到删种条件:%s,删除任务..." % (torrent.get('name'), delete_type.value)) + if sendmessage: + __send_message(task_name, delete_type, torrent.get('name')) + + if torrent_id not in delete_ids: + delete_ids.append(torrent_id) + update_torrents.append(("%s,%s" % (uploaded, downloaded), taskid, torrent_id)) + # 检查下载中状态的 + torrents, has_err = downloader.get_torrents(ids=torrent_ids, status=["downloading"]) + # 看看是否有错误, 有错误的话就不处理了 + if has_err: + log.warn("【BRUSH】任务 %s 获取种子状态失败" % task_name) + continue + remove_torrent_ids = list( + set(remove_torrent_ids).difference(set([torrent.get("hash") for torrent in torrents]))) + for torrent in torrents: + # ID + torrent_id = torrent.get("hash") + # 下载耗时 秒 + dltime = int(time.time() - torrent.get("added_on")) + # 上传量 Byte + uploaded = torrent.get("uploaded") or 0 + total_uploaded += uploaded + # 平均上传速度 Byte/s + avg_upspeed = int(uploaded / dltime) + # 已未活动 秒 + date_now = int(time.mktime(datetime.now().timetuple())) + last_activity = int(torrent.get("last_activity", 0)) + iatime = date_now - last_activity if last_activity else 0 + # 下载量 + downloaded = torrent.get("downloaded") + total_downloaded += downloaded + need_delete, delete_type = self.__check_remove_rule(remove_rule=remove_rule, + dltime=dltime, + avg_upspeed=avg_upspeed, + iatime=iatime) + if need_delete: + log.info( + "【Brush】%s 达到删种条件:%s,删除下载任务..." % (torrent.get('name'), delete_type.value)) + if sendmessage: + __send_message(task_name, delete_type, torrent.get('name')) + + if torrent_id not in delete_ids: + delete_ids.append(torrent_id) + update_torrents.append(("%s,%s" % (uploaded, downloaded), taskid, torrent_id)) + # transmission + else: + # 将查询的torrent_ids转为数字型 + torrent_ids = [int(x) for x in torrent_ids if str(x).isdigit()] + # 检查完成状态 + downloader = Transmission(config=downloader_cfg) + torrents, has_err = downloader.get_torrents(ids=torrent_ids, status=["seeding", "seed_pending"]) + # 看看是否有错误, 有错误的话就不处理了 + if has_err: + log.warn("【BRUSH】任务 %s 获取种子状态失败" % task_name) + continue + remove_torrent_ids = list(set(torrent_ids).difference(set([torrent.id for torrent in torrents]))) + for torrent in torrents: + # ID + torrent_id = torrent.id + # 做种时间 + date_done = torrent.date_done or torrent.date_added + date_now = int(time.mktime(datetime.now().timetuple())) + dltime = date_now - int(time.mktime(torrent.date_added.timetuple())) + seeding_time = date_now - int(time.mktime(date_done.timetuple())) + # 下载量 + downloaded = int(torrent.total_size * torrent.progress / 100) + total_downloaded += downloaded + # 分享率 + ratio = torrent.ratio or 0 + # 上传量 + uploaded = int(downloaded * torrent.ratio) + total_uploaded += uploaded + # 平均上传速度 + avg_upspeed = int(uploaded / dltime) + need_delete, delete_type = self.__check_remove_rule(remove_rule=remove_rule, + seeding_time=seeding_time, + ratio=ratio, + uploaded=uploaded, + avg_upspeed=avg_upspeed) + if need_delete: + log.info("【Brush】%s 做种达到删种条件:%s,删除任务..." % (torrent.name, delete_type.value)) + if sendmessage: + __send_message(task_name, delete_type, torrent.name) + + if torrent_id not in delete_ids: + delete_ids.append(torrent_id) + update_torrents.append(("%s,%s" % (uploaded, downloaded), taskid, torrent_id)) + # 检查下载状态 + torrents, has_err = downloader.get_torrents(ids=torrent_ids, + status=["downloading", "download_pending", "stopped"]) + # 看看是否有错误, 有错误的话就不处理了 + if has_err: + log.warn("【BRUSH】任务 %s 获取种子状态失败" % task_name) + continue + remove_torrent_ids = list( + set(remove_torrent_ids).difference(set([torrent.id for torrent in torrents]))) + for torrent in torrents: + # ID + torrent_id = torrent.id + # 下载耗时 + dltime = (datetime.now().astimezone() - torrent.date_added).seconds + # 下载量 + downloaded = int(torrent.total_size * torrent.progress / 100) + total_downloaded += downloaded + # 上传量 + uploaded = int(downloaded * torrent.ratio) + total_uploaded += uploaded + # 平均上传速度 + avg_upspeed = int(uploaded / dltime) + need_delete, delete_type = self.__check_remove_rule(remove_rule=remove_rule, + dltime=dltime, + avg_upspeed=avg_upspeed) + if need_delete: + log.info("【Brush】%s 达到删种条件:%s,删除下载任务..." % (torrent.name, delete_type.value)) + if sendmessage: + __send_message(task_name, delete_type, torrent.name) + + if torrent_id not in delete_ids: + delete_ids.append(torrent_id) + update_torrents.append(("%s,%s" % (uploaded, downloaded), taskid, torrent_id)) + # 手工删除的种子,清除对应记录 + if remove_torrent_ids: + log.info("【Brush】任务 %s 的这些下载任务在下载器中不存在,将删除任务记录:%s" % ( + task_name, remove_torrent_ids)) + for remove_torrent_id in remove_torrent_ids: + self.dbhelper.delete_brushtask_torrent(taskid, remove_torrent_id) + # 更新种子状态为已删除 + self.dbhelper.update_brushtask_torrent_state(update_torrents) + # 删除下载器种子 + if delete_ids: + downloader.delete_torrents(delete_file=True, ids=delete_ids) + log.info("【Brush】任务 %s 共删除 %s 个刷流下载任务" % (task_name, len(delete_ids))) + else: + log.info("【Brush】任务 %s 本次检查未删除下载任务" % task_name) + # 更新上传下载量和删除种子数 + self.dbhelper.add_brushtask_upload_count(brush_id=taskid, + upload_size=total_uploaded, + download_size=total_downloaded, + remove_count=len(delete_ids) + len(remove_torrent_ids)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + + def __is_allow_new_torrent(self, taskid, taskname, downloadercfg, seedsize, dlcount): + """ + 检查是否还能添加新的下载 + """ + if not taskid: + return False + # 判断大小 + total_size = self.dbhelper.get_brushtask_totalsize(taskid) + if seedsize: + if float(seedsize) * 1024 ** 3 <= int(total_size): + log.warn("【Brush】刷流任务 %s 当前保种体积 %sGB,不再新增下载" + % (taskname, round(int(total_size) / 1024 / 1024 / 1024, 1))) + return False + # 检查正在下载的任务数 + if dlcount: + downloading_count = self.__get_downloading_count(downloadercfg) + if downloading_count is None: + log.error("【Brush】任务 %s 下载器 %s 无法连接" % (taskname, downloadercfg.get("name"))) + return False + if int(downloading_count) >= int(dlcount): + log.warn("【Brush】下载器 %s 正在下载任务数:%s,超过设定上限,暂不添加下载" % ( + downloadercfg.get("name"), downloading_count)) + return False + return True + + def get_downloader_info(self, dlid=None): + """ + 获取下载器的参数 + """ + if dlid: + for downloader in self._downloader_infos: + if downloader.get('id') == int(dlid): + if downloader.get('type') == self._qb_client: + return { + "id": downloader.get("id"), + "name": downloader.get("name"), + "type": downloader.get("type"), + "save_dir": downloader.get("save_dir"), + "qbhost": downloader.get("host"), + "qbport": downloader.get("port"), + "qbusername": downloader.get("username"), + "qbpassword": downloader.get("password") + } + elif downloader.get('type') == self._tr_client: + return { + "id": downloader.get("id"), + "name": downloader.get("name"), + "type": downloader.get("type"), + "save_dir": downloader.get("save_dir"), + "trhost": downloader.get("host"), + "trport": downloader.get("port"), + "trusername": downloader.get("username"), + "trpassword": downloader.get("password") + } + return downloader + return {} + else: + return self._downloader_infos + + def __get_downloading_count(self, downloadercfg): + """ + 查询当前正在下载的任务数 + """ + if not downloadercfg: + return 0 + if downloadercfg.get("type") == self._qb_client: + downloader = Qbittorrent(config=downloadercfg) + if not downloader.qbc: + return None + dlitems = downloader.get_downloading_torrents() + if dlitems is not None: + return int(len(dlitems)) + else: + downloader = Transmission(config=downloadercfg) + if not downloader.trc: + return None + dlitems = downloader.get_downloading_torrents() + if dlitems is not None: + return int(len(dlitems)) + return None + + def __download_torrent(self, + downloadercfg, + title, + enclosure, + size, + taskid, + transfer, + sendmessage, + forceupload, + upspeed, + downspeed, + taskname, + site_info): + """ + 添加下载任务,更新任务数据 + :param downloadercfg: 下载器的所有参数 + :param title: 种子名称 + :param enclosure: 种子地址 + :param size: 种子大小 + :param taskid: 任务ID + :param transfer: 是否要转移,为False时直接添加已整理的标签 + :param sendmessage: 是否需要消息推送 + :param forceupload: 是否需要将添加的刷流任务设置为强制做种(仅针对qBittorrent) + :param upspeed: 上传限速 + :param downspeed: 下载限速 + :param taskname: 任务名称 + :param site_info: 站点信息 + """ + if not downloadercfg or not enclosure: + return False + # 标签 + tag = "已整理" if not transfer else None + # 下载任务ID + download_id = None + # 下载种子文件 + _, content, _, _, retmsg = Torrent().get_torrent_info( + url=enclosure, + cookie=site_info.get("cookie"), + ua=site_info.get("ua"), + proxy=site_info.get("proxy")) + if content: + # 添加下载 + if downloadercfg.get("type") == self._qb_client: + # 初始化下载器 + downloader = Qbittorrent(config=downloadercfg) + if not downloader.qbc: + log.error("【Brush】任务 %s 下载器 %s 无法连接" % (taskname, downloadercfg.get("name"))) + return False + torrent_tag = "NT" + StringUtils.generate_random_str(5) + if tag: + tags = [tag, torrent_tag] + else: + tags = torrent_tag + ret = downloader.add_torrent(content=content, + tag=tags, + download_dir=downloadercfg.get("save_dir"), + upload_limit=upspeed, + download_limit=downspeed) + if ret: + # QB添加下载后需要时间,重试5次每次等待5秒 + download_id = downloader.get_torrent_id_by_tag(torrent_tag) + if download_id: + # 开始下载 + downloader.start_torrents(download_id) + # 强制做种 + if forceupload: + downloader.torrents_set_force_start(download_id) + else: + # 初始化下载器 + downloader = Transmission(config=downloadercfg) + if not downloader.trc: + log.error("【Brush】任务 %s 下载器 %s 无法连接" % (taskname, downloadercfg.get("name"))) + return False + ret = downloader.add_torrent(content=content, + download_dir=downloadercfg.get("save_dir"), + upload_limit=upspeed, + download_limit=downspeed + ) + if ret: + download_id = ret.id + # 设置标签 + if download_id and tag: + downloader.set_torrent_tag(tid=download_id, tag=tag) + if not download_id: + # 下载失败 + log.warn(f"【Brush】{taskname} 添加下载任务出错:{title}," + f"错误原因:{retmsg or '下载器添加任务失败'}," + f"种子链接:{enclosure}") + return False + else: + # 下载成功 + log.info("【Brush】成功添加下载:%s" % title) + if sendmessage: + msg_title = "【刷流任务 {} 新增下载】".format(taskname) + msg_text = "种子名称:{}\n种子大小:{}".format(title, StringUtils.str_filesize(size)) + self.message.send_brushtask_added_message(title=msg_title, text=msg_text) + # 插入种子数据 + if self.dbhelper.insert_brushtask_torrent(brush_id=taskid, + title=title, + enclosure=enclosure, + downloader=downloadercfg.get("id"), + download_id=download_id, + size=size): + # 更新下载次数 + self.dbhelper.add_brushtask_download_count(brush_id=taskid) + else: + log.info("【Brush】%s 已下载过" % title) + + return True + + def __check_rss_rule(self, + rss_rule, + title, + torrent_url, + torrent_size, + pubdate, + cookie, + ua, + proxy): + """ + 检查种子是否符合刷流过滤条件 + :param rss_rule: 过滤条件字典 + :param title: 种子名称 + :param torrent_url: 种子页面地址 + :param torrent_size: 种子大小 + :param pubdate: 发布时间 + :param cookie: Cookie + :param ua: User-Agent + :return: 是否命中 + """ + if not rss_rule: + return True + # 检查种子大小 + try: + if rss_rule.get("size"): + rule_sizes = rss_rule.get("size").split("#") + if rule_sizes[0]: + if len(rule_sizes) > 1 and rule_sizes[1]: + min_max_size = rule_sizes[1].split(',') + min_size = min_max_size[0] + if len(min_max_size) > 1: + max_size = min_max_size[1] + else: + max_size = 0 + if rule_sizes[0] == "gt" and float(torrent_size) < float(min_size) * 1024 ** 3: + return False + if rule_sizes[0] == "lt" and float(torrent_size) > float(min_size) * 1024 ** 3: + return False + if rule_sizes[0] == "bw" and not float(min_size) * 1024 ** 3 < float(torrent_size) < float( + max_size) * 1024 ** 3: + return False + + # 检查包含规则 + if rss_rule.get("include"): + if not re.search(r"%s" % rss_rule.get("include"), title): + return False + + # 检查排除规则 + if rss_rule.get("exclude"): + if re.search(r"%s" % rss_rule.get("exclude"), title): + return False + + torrent_attr = self.sites.check_torrent_attr(torrent_url=torrent_url, + cookie=cookie, + ua=ua, + proxy=proxy) + torrent_peer_count = torrent_attr.get("peer_count") + log.debug("【Brush】%s 解析详情, %s" % (title, torrent_attr)) + + # 检查免费状态 + if rss_rule.get("free") == "FREE": + if not torrent_attr.get("free"): + log.debug("【Brush】不是一个FREE资源,跳过") + return False + elif rss_rule.get("free") == "2XFREE": + if not torrent_attr.get("2xfree"): + log.debug("【Brush】不是一个2XFREE资源,跳过") + return False + + # 检查HR状态 + if rss_rule.get("hr"): + if torrent_attr.get("hr"): + log.debug("【Brush】这是一个H&R资源,跳过") + return False + + # 检查做种人数 + if rss_rule.get("peercount"): + # 兼容旧版本 + peercount_str = rss_rule.get("peercount") + if not peercount_str: + peercount_str = "#" + elif "#" not in peercount_str: + peercount_str = "lt#" + peercount_str + else: + pass + peer_counts = peercount_str.split("#") + if len(peer_counts) >= 2 and peer_counts[1]: + min_max_count = peer_counts[1].split(',') + min_count = int(min_max_count[0]) + if len(min_max_count) > 1: + max_count = int(min_max_count[1]) + else: + max_count = sys.maxsize + if peer_counts[0] == "gt" and torrent_peer_count <= min_count: + log.debug("【Brush】%s `判断做种数, 判断条件: peer_count:%d %s threshold:%d" % ( + title, torrent_peer_count, peer_counts[0], min_count)) + return False + if peer_counts[0] == "lt" and torrent_peer_count >= min_count: + log.debug("【Brush】%s `判断做种数, 判断条件: peer_count:%d %s threshold:%d" % ( + title, torrent_peer_count, peer_counts[0], min_count)) + return False + if peer_counts[0] == "bw" and not (min_count <= torrent_peer_count <= max_count): + log.debug("【Brush】%s `判断做种数, 判断条件: left:%d %s peer_count:%d %s right:%d" % ( + title, min_count, peer_counts[0], torrent_peer_count, peer_counts[0], max_count)) + return False + + # 检查发布时间 + if rss_rule.get("pubdate") and pubdate: + rule_pubdates = rss_rule.get("pubdate").split("#") + if len(rule_pubdates) >= 2 and rule_pubdates[1]: + localtz = pytz.timezone(Config().get_timezone()) + localnowtime = datetime.now().astimezone(localtz) + localpubdate = pubdate.astimezone(localtz) + log.debug('【Brush】发布时间:%s,当前时间:%s' % (localpubdate.isoformat(), localnowtime.isoformat())) + if (localnowtime - localpubdate).seconds / 3600 > float(rule_pubdates[1]): + log.debug("【Brush】发布时间不符合条件。") + return False + + except Exception as err: + ExceptionUtils.exception_traceback(err) + + return True + + @staticmethod + def __check_remove_rule(remove_rule, seeding_time=None, ratio=None, uploaded=None, dltime=None, avg_upspeed=None, iatime=None): + """ + 检查是否符合删种规则 + :param remove_rule: 删种规则 + :param seeding_time: 做种时间 + :param ratio: 分享率 + :param uploaded: 上传量 + :param dltime: 下载耗时 + :param avg_upspeed: 上传平均速度 + :param iatime: 未活动时间 + """ + if not remove_rule: + return False + try: + if remove_rule.get("time") and seeding_time: + rule_times = remove_rule.get("time").split("#") + if rule_times[0]: + if len(rule_times) > 1 and rule_times[1]: + if float(seeding_time) > float(rule_times[1]) * 3600: + return True, BrushDeleteType.SEEDTIME + if remove_rule.get("ratio") and ratio: + rule_ratios = remove_rule.get("ratio").split("#") + if rule_ratios[0]: + if len(rule_ratios) > 1 and rule_ratios[1]: + if float(ratio) > float(rule_ratios[1]): + return True, BrushDeleteType.RATIO + if remove_rule.get("uploadsize") and uploaded: + rule_uploadsizes = remove_rule.get("uploadsize").split("#") + if rule_uploadsizes[0]: + if len(rule_uploadsizes) > 1 and rule_uploadsizes[1]: + if float(uploaded) > float(rule_uploadsizes[1]) * 1024 ** 3: + return True, BrushDeleteType.UPLOADSIZE + if remove_rule.get("dltime") and dltime: + rule_times = remove_rule.get("dltime").split("#") + if rule_times[0]: + if len(rule_times) > 1 and rule_times[1]: + if float(dltime) > float(rule_times[1]) * 3600: + return True, BrushDeleteType.DLTIME + if remove_rule.get("avg_upspeed") and avg_upspeed: + rule_avg_upspeeds = remove_rule.get("avg_upspeed").split("#") + if rule_avg_upspeeds[0]: + if len(rule_avg_upspeeds) > 1 and rule_avg_upspeeds[1]: + if float(avg_upspeed) < float(rule_avg_upspeeds[1]) * 1024: + return True, BrushDeleteType.AVGUPSPEED + if remove_rule.get("iatime") and iatime: + rule_times = remove_rule.get("iatime").split("#") + if rule_times[0]: + if len(rule_times) > 1 and rule_times[1]: + if float(iatime) > float(rule_times[1]) * 3600: + return True, BrushDeleteType.IATIME + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False, BrushDeleteType.NOTDELETE diff --git a/app/conf/__init__.py b/app/conf/__init__.py new file mode 100644 index 0000000..f99c6eb --- /dev/null +++ b/app/conf/__init__.py @@ -0,0 +1,3 @@ +from .systemconfig import SystemConfig +from .moduleconf import ModuleConf +from .siteconf import SiteConf diff --git a/app/conf/moduleconf.py b/app/conf/moduleconf.py new file mode 100644 index 0000000..c7c6af6 --- /dev/null +++ b/app/conf/moduleconf.py @@ -0,0 +1,917 @@ +# coding: utf-8 +from app.utils.types import * + + +class ModuleConf(object): + # 菜单对应关系,配置WeChat应用中配置的菜单ID与执行命令的对应关系,需要手工修改 + # 菜单序号在https://work.weixin.qq.com/wework_admin/frame#apps 应用自定义菜单中维护,然后看日志输出的菜单序号是啥(按顺利能猜到的).... + # 命令对应关系:/ptt 下载文件转移;/ptr 删种;/pts 站点签到;/rst 目录同步;/rst 豆瓣想看;/utf 重新识别; + # /ssa 订阅搜索;/tbl 清理转移缓存;/trh 清理RSS缓存;/rss RSS下载;/udt 系统更新 + WECHAT_MENU = { + '_0_0': '/ptt', + '_0_1': '/ptr', + '_0_2': '/rss', + '_0_3': '/ssa', + '_1_0': '/rst', + '_1_1': '/db', + '_1_2': '/utf', + '_2_0': '/pts', + '_2_1': '/udt', + '_2_2': '/tbl', + '_2_3': '/trh' + } + + # 全量转移模式 + RMT_MODES = { + "copy": RmtMode.COPY, + "link": RmtMode.LINK, + "softlink": RmtMode.SOFTLINK, + "move": RmtMode.MOVE, + "rclone": RmtMode.RCLONE, + "rclonecopy": RmtMode.RCLONECOPY, + "minio": RmtMode.MINIO, + "miniocopy": RmtMode.MINIOCOPY + } + + # 精简版转移模式 + RMT_MODES_LITE = { + "copy": RmtMode.COPY, + "link": RmtMode.LINK, + "softlink": RmtMode.SOFTLINK, + "move": RmtMode.MOVE + } + + # 下载器 + DOWNLOADER_DICT = { + "qbittorrent": DownloaderType.QB, + "transmission": DownloaderType.TR, + "client115": DownloaderType.Client115, + "pikpak": DownloaderType.PikPak + } + + # 索引器 + INDEXER_DICT = { + "builtin": IndexerType.BUILTIN + } + + # 媒体服务器 + MEDIASERVER_DICT = { + "emby": MediaServerType.EMBY, + "jellyfin": MediaServerType.JELLYFIN, + "plex": MediaServerType.PLEX + } + + # 消息通知类型 + MESSAGE_CONF = { + "client": { + "telegram": { + "name": "Telegram", + "img_url": "../static/img/telegram.png", + "search_type": SearchType.TG, + "config": { + "token": { + "id": "telegram_token", + "required": True, + "title": "Bot Token", + "tooltip": "telegram机器人的Token,关注BotFather创建机器人", + "type": "text" + }, + "chat_id": { + "id": "telegram_chat_id", + "required": True, + "title": "Chat ID", + "tooltip": "接受消息通知的用户、群组或频道Chat ID,关注@getidsbot获取", + "type": "text" + }, + "user_ids": { + "id": "telegram_user_ids", + "required": False, + "title": "User IDs", + "tooltip": "允许使用交互的用户Chat ID,留空则只允许管理用户使用,关注@getidsbot获取", + "type": "text", + "placeholder": "使用,分隔多个Id" + }, + "admin_ids": { + "id": "telegram_admin_ids", + "required": False, + "title": "Admin IDs", + "tooltip": "允许使用管理命令的用户Chat ID,关注@getidsbot获取", + "type": "text", + "placeholder": "使用,分隔多个Id" + }, + "webhook": { + "id": "telegram_webhook", + "required": False, + "title": "Webhook", + "tooltip": "Telegram机器人消息有两种模式:Webhook或消息轮循;开启后将使用Webhook方式,需要在基础设置中正确配置好外网访问地址,同时受Telegram官方限制,外网访问地址需要设置为以下端口之一:443, 80, 88, 8443,且需要有公网认证的可信SSL证书;关闭后将使用消息轮循方式,使用该方式需要在基础设置->安全处将Telegram ipv4源地址设置为127.0.0.1,如同时使用了内置的SSL证书功能,消息轮循方式可能无法正常使用", + "type": "switch" + } + } + }, + "wechat": { + "name": "微信", + "img_url": "../static/img/wechat.png", + "search_type": SearchType.WX, + "config": { + "corpid": { + "id": "wechat_corpid", + "required": True, + "title": "企业ID", + "tooltip": "每个企业都拥有唯一的corpid,获取此信息可在管理后台“我的企业”-“企业信息”下查看“企业ID”(需要有管理员权限)", + "type": "text" + }, + "corpsecret": { + "id": "wechat_corpsecret", + "required": True, + "title": "应用Secret", + "tooltip": "每个应用都拥有唯一的secret,获取此信息可在管理后台“应用与小程序”-“自建”下查看“Secret”(需要有管理员权限)", + "type": "text", + "placeholder": "Secret" + }, + "agentid": { + "id": "wechat_agentid", + "required": True, + "title": "应用ID", + "tooltip": "每个应用都拥有唯一的agentid,获取此信息可在管理后台“应用与小程序”-“自建”下查看“AgentId”(需要有管理员权限)", + "type": "text", + "placeholder": "AgentId", + }, + "default_proxy": { + "id": "wechat_default_proxy", + "required": False, + "title": "消息推送代理", + "tooltip": "由于微信官方限制,2022年6月20日后创建的企业微信应用需要有固定的公网IP地址并加入IP白名单后才能发送消息,使用有固定公网IP的代理服务器转发可解决该问题;代理服务器需自行搭建,搭建方法可参考项目主页说明", + "type": "text", + "placeholder": "https://wechat.nastool.cn" + }, + "token": { + "id": "wechat_token", + "required": False, + "title": "Token", + "tooltip": "需要交互功能时才需要填写,在微信企业应用管理后台-接收消息设置页面生成,填入完成后重启本应用,然后再在微信页面输入地址确定", + "type": "text", + "placeholder": "API接收消息Token" + }, + "encodingAESKey": { + "id": "wechat_encodingAESKey", + "required": False, + "title": "EncodingAESKey", + "tooltip": "需要交互功能时才需要填写,在微信企业应用管理后台-接收消息设置页面生成,填入完成后重启本应用,然后再在微信页面输入地址确定", + "type": "text", + "placeholder": "API接收消息EncodingAESKey" + } + } + }, + "serverchan": { + "name": "Server酱", + "img_url": "../static/img/serverchan.png", + "config": { + "sckey": { + "id": "serverchan_sckey", + "required": True, + "title": "SCKEY", + "tooltip": "填写ServerChan的API Key,SCT类型,在https://sct.ftqq.com/中申请", + "type": "text", + "placeholder": "SCT..." + } + } + }, + "bark": { + "name": "Bark", + "img_url": "../static/img/bark.webp", + "config": { + "server": { + "id": "bark_server", + "required": True, + "title": "Bark服务器地址", + "tooltip": "自己搭建Bark服务端请实际配置,否则可使用:https://api.day.app", + "type": "text", + "placeholder": "https://api.day.app", + "default": "https://api.day.app" + }, + "apikey": { + "id": "bark_apikey", + "required": True, + "title": "API Key", + "tooltip": "在Bark客户端中点击右上角的“...”按钮,选择“生成Bark Key”,然后将生成的KEY填入此处", + "type": "text" + }, + "params": { + "id": "bark_params", + "required": False, + "title": "附加参数", + "tooltip": "添加到Bark通知中的附加参数,可用于自定义通知特性", + "type": "text", + "placeholder": "group=xxx&sound=xxx&url=xxx" + } + } + }, + "pushdeer": { + "name": "PushDeer", + "img_url": "../static/img/pushdeer.png", + "config": { + "server": { + "id": "pushdeer_server", + "required": True, + "title": "PushDeer服务器地址", + "tooltip": "自己搭建pushdeer服务端请实际配置,否则可使用:https://api2.pushdeer.com", + "type": "text", + "placeholder": "https://api2.pushdeer.com", + "default": "https://api2.pushdeer.com" + }, + "apikey": { + "id": "pushdeer_apikey", + "required": True, + "title": "API Key", + "tooltip": "pushdeer客户端生成的KEY", + "type": "text" + } + } + }, + "pushplus": { + "name": "PushPlus", + "img_url": "../static/img/pushplus.jpg", + "config": { + "token": { + "id": "pushplus_token", + "required": True, + "title": "Token", + "tooltip": "在PushPlus官网中申请,申请地址:http://pushplus.plus/", + "type": "text" + }, + "channel": { + "id": "pushplus_channel", + "required": True, + "title": "推送渠道", + "tooltip": "使用PushPlus中配置的发送渠道,具体参考pushplus.plus官网文档说明,支持第三方webhook、钉钉、飞书、邮箱等", + "type": "select", + "options": { + "wechat": "微信", + "mail": "邮箱", + "webhook": "第三方Webhook" + }, + "default": "wechat" + }, + "topic": { + "id": "pushplus_topic", + "required": False, + "title": "群组编码", + "tooltip": "PushPlus中创建的群组,如未设置可为空", + "type": "text" + }, + "webhook": { + "id": "pushplus_webhook", + "required": False, + "title": "Webhook编码", + "tooltip": "PushPlus中创建的webhook编码,发送渠道为第三方webhook时需要填入", + } + } + }, + "iyuu": { + "name": "爱语飞飞", + "img_url": "../static/img/iyuu.png", + "config": { + "token": { + "id": "iyuumsg_token", + "required": True, + "title": "令牌Token", + "tooltip": "在爱语飞飞官网中申请,申请地址:https://iyuu.cn/", + "type": "text", + "placeholder": "登录https://iyuu.cn获取" + } + } + }, + "slack": { + "name": "Slack", + "img_url": "../static/img/slack.png", + "search_type": SearchType.SLACK, + "config": { + "bot_token": { + "id": "slack_bot_token", + "required": True, + "title": "Bot User OAuth Token", + "tooltip": "在Slack中创建应用,获取Bot User OAuth Token", + "type": "text", + "placeholder": "xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx" + }, + "app_token": { + "id": "slack_app_token", + "required": True, + "title": "App-Level Token", + "tooltip": "在Slack中创建应用,获取App-Level Token", + "type": "text", + "placeholder": "xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx" + }, + "channel": { + "id": "slack_channel", + "required": False, + "title": "频道名称", + "tooltip": "Slack中的频道名称,默认为全体;需要将机器人添加到该频道,以接收非交互类的通知消息", + "type": "text", + "placeholder": "全体" + } + } + }, + "gotify": { + "name": "Gotify", + "img_url": "../static/img/gotify.png", + "config": { + "server": { + "id": "gotify_server", + "required": True, + "title": "Gotify服务器地址", + "tooltip": "自己搭建gotify服务端地址", + "type": "text", + "placeholder": "http://localhost:8800" + }, + "token": { + "id": "gotify_token", + "required": True, + "title": "令牌Token", + "tooltip": "Gotify服务端APPS下创建的token", + "type": "text" + }, + "priority": { + "id": "gotify_priority", + "required": False, + "title": "消息Priority", + "tooltip": "消息通知优先级, 请填写数字(1-8), 默认: 8", + "type": "text", + "placeholder": "8" + } + } + }, + "chanify": { + "name": "Chanify", + "img_url": "../static/img/chanify.png", + "config": { + "server": { + "id": "chanify_server", + "required": True, + "title": "Chanify服务器地址", + "tooltip": "自己搭建Chanify服务端地址或使用https://api.chanify.net", + "type": "text", + "placeholder": "https://api.chanify.net", + "default": "https://api.chanify.net" + }, + "token": { + "id": "chanify_token", + "required": True, + "title": "令牌", + "tooltip": "在Chanify客户端频道中获取", + "type": "text" + } + } + }, + "synologychat": { + "name": "Synology Chat", + "img_url": "../static/img/synologychat.png", + "search_type": SearchType.SYNOLOGY, + "config": { + "webhook_url": { + "id": "synologychat_webhook_url", + "required": True, + "title": "机器人传入URL", + "tooltip": "在Synology Chat中创建机器人,获取机器人传入URL", + "type": "text", + "placeholder": "https://xxx/webapi/entry.cgi?api=xxx" + }, + "token": { + "id": "synologychat_token", + "required": True, + "title": "令牌", + "tooltip": "在Synology Chat中创建机器人,获取机器人令牌", + "type": "text", + "placeholder": "" + } + } + }, + }, + "switch": { + "download_start": { + "name": "新增下载", + "fuc_name": "download_start" + }, + "download_fail": { + "name": "下载失败", + "fuc_name": "download_fail" + }, + "transfer_finished": { + "name": "入库完成", + "fuc_name": "transfer_finished" + }, + "transfer_fail": { + "name": "入库失败", + "fuc_name": "transfer_fail" + }, + "rss_added": { + "name": "新增订阅", + "fuc_name": "rss_added" + }, + "rss_finished": { + "name": "订阅完成", + "fuc_name": "rss_finished" + }, + "site_signin": { + "name": "站点签到", + "fuc_name": "site_signin" + }, + "site_message": { + "name": "站点消息", + "fuc_name": "site_message" + }, + "brushtask_added": { + "name": "刷流下种", + "fuc_name": "brushtask_added" + }, + "brushtask_remove": { + "name": "刷流删种", + "fuc_name": "brushtask_remove" + }, + "mediaserver_message": { + "name": "媒体服务", + "fuc_name": "mediaserver_message" + }, + "custom_message": { + "name": "自定义消息", + "fuc_name": "custom_message" + } + } + } + + # 自动删种配置 + TORRENTREMOVER_DICT = { + "Qb": { + "name": "Qbittorrent", + "img_url": "../static/img/qbittorrent.png", + "downloader_type": DownloaderType.QB, + "torrent_state": { + "downloading": "正在下载_传输数据", + "stalledDL": "正在下载_未建立连接", + "uploading": "正在上传_传输数据", + "stalledUP": "正在上传_未建立连接", + "error": "暂停_发生错误", + "pausedDL": "暂停_下载未完成", + "pausedUP": "暂停_下载完成", + "missingFiles": "暂停_文件丢失", + "checkingDL": "检查中_下载未完成", + "checkingUP": "检查中_下载完成", + "checkingResumeData": "检查中_启动时恢复数据", + "forcedDL": "强制下载_忽略队列", + "queuedDL": "等待下载_排队", + "forcedUP": "强制上传_忽略队列", + "queuedUP": "等待上传_排队", + "allocating": "分配磁盘空间", + "metaDL": "获取元数据", + "moving": "移动文件", + "unknown": "未知状态", + } + }, + "Tr": { + "name": "Transmission", + "img_url": "../static/img/transmission.png", + "downloader_type": DownloaderType.TR, + "torrent_state": { + "downloading": "正在下载", + "seeding": "正在上传", + "download_pending": "等待下载_排队", + "seed_pending": "等待上传_排队", + "checking": "正在检查", + "check_pending": "等待检查_排队", + "stopped": "暂停", + } + } + } + + # 搜索种子过滤属性 + TORRENT_SEARCH_PARAMS = { + "restype": { + "BLURAY": r"Blu-?Ray|BD|BDRIP", + "REMUX": r"REMUX", + "DOLBY": r"DOLBY|DOVI|\s+DV$|\s+DV\s+", + "WEB": r"WEB-?DL|WEBRIP", + "HDTV": r"U?HDTV", + "UHD": r"UHD", + "HDR": r"HDR", + "3D": r"3D" + }, + "pix": { + "8k": r"8K", + "4k": r"4K|2160P|X2160", + "1080p": r"1080[PIX]|X1080", + "720p": r"720P" + } + } + + # 网络测试对象 + NETTEST_TARGETS = [ + "www.themoviedb.org", + "api.themoviedb.org", + "api.tmdb.org", + "image.tmdb.org", + "webservice.fanart.tv", + "api.telegram.org", + "qyapi.weixin.qq.com", + "www.opensubtitles.org" + ] + + # 下载器 + DOWNLOADER_CONF = { + "qbittorrent": { + "name": "Qbittorrent", + "img_url": "../static/img/qbittorrent.png", + "background": "bg-blue", + "test_command": "app.downloader.client.qbittorrent|Qbittorrent", + "config": { + "qbhost": { + "id": "qbittorrent.qbhost", + "required": True, + "title": "IP地址", + "tooltip": "配置IP地址,如为https则需要增加https://前缀", + "type": "text", + "placeholder": "127.0.0.1" + }, + "qbport": { + "id": "qbittorrent.qbport", + "required": True, + "title": "端口", + "type": "text", + "placeholder": "8080" + }, + "qbusername": { + "id": "qbittorrent.qbusername", + "required": True, + "title": "用户名", + "type": "text", + "placeholder": "admin" + }, + "qbpassword": { + "id": "qbittorrent.qbpassword", + "required": False, + "title": "密码", + "type": "password", + "placeholder": "adminadmin" + }, + "force_upload": { + "id": "qbittorrent.force_upload", + "required": False, + "title": "自动强制作种", + "tooltip": "开启后下载文件转移完成时会自动将对应种子设置为强制做种状态,需在基础设置中开启下载软件监控功能", + "type": "switch" + }, + "auto_management": { + "id": "qbittorrent.auto_management", + "required": False, + "title": "自动管理模式", + "tooltip": "开启后下载目录将由Qbittorrent自动管理,不再使用NASTool传递的下载目录,需要同时在下载目录设置中配置好分类标签", + "type": "switch" + } + } + }, + "transmission": { + "name": "Transmission", + "img_url": "../static/img/transmission.png", + "background": "bg-danger", + "test_command": "app.downloader.client.transmission|Transmission", + "config": { + "trhost": { + "id": "transmission.trhost", + "required": True, + "title": "IP地址", + "tooltip": "配置IP地址,如为https则需要增加https://前缀", + "type": "text", + "placeholder": "127.0.0.1" + }, + "trport": { + "id": "transmission.trport", + "required": True, + "title": "端口", + "type": "text", + "placeholder": "9091" + }, + "trusername": { + "id": "transmission.trusername", + "required": True, + "title": "用户名", + "type": "text", + "placeholder": "admin" + }, + "trpassword": { + "id": "transmission.trpassword", + "required": False, + "title": "密码", + "type": "password", + "placeholder": "" + } + } + }, + "client115": { + "name": "115网盘", + "img_url": "../static/img/115.jpg", + "background": "bg-azure", + "test_command": "app.downloader.client.client115|Client115", + "config": { + "cookie": { + "id": "client115.cookie", + "required": True, + "title": "Cookie", + "tooltip": "115网盘Cookie,通过115网盘网页端抓取Cookie", + "type": "text", + "placeholder": "USERSESSIONID=xxx;115_lang=zh;UID=xxx;CID=xxx;SEID=xxx" + } + } + }, + "pikpak": { + "name": "PikPak", + "img_url": "../static/img/pikpak.png", + "background": "bg-indigo", + "test_command": "app.downloader.client.pikpak|PikPak", + "config": { + "username": { + "id": "pikpak.username", + "required": True, + "title": "用户名", + "tooltip": "用户名", + "type": "text", + "placeholder": "" + }, + "password": { + "id": "pikpak.password", + "required": True, + "title": "密码", + "tooltip": "密码", + "type": "password", + "placeholder": "" + }, + "proxy": { + "id": "pikpak.proxy", + "required": False, + "title": "代理", + "tooltip": "如果需要代理才能访问pikpak可以在此处填入代理地址", + "type": "text", + "placeholder": "127.0.0.1:7890" + } + } + }, + } + + # 媒体服务器 + MEDIASERVER_CONF = { + "emby": { + "name": "Emby", + "img_url": "../static/img/emby.png", + "background": "bg-green", + "test_command": "app.mediaserver.client.emby|Emby", + "config": { + "host": { + "id": "emby.host", + "required": True, + "title": "服务器地址", + "tooltip": "配置IP地址和端口,如为https则需要增加https://前缀", + "type": "text", + "placeholder": "http://127.0.0.1:8096" + }, + "api_key": { + "id": "emby.api_key", + "required": True, + "title": "Api Key", + "tooltip": "在Emby设置->高级->API密钥处生成,注意不要复制到了应用名称", + "type": "text", + "placeholder": "" + } + } + }, + "jellyfin": { + "name": "Jellyfin", + "img_url": "../static/img/jellyfin.jpg", + "background": "bg-purple", + "test_command": "app.mediaserver.client.jellyfin|Jellyfin", + "config": { + "host": { + "id": "jellyfin.host", + "required": True, + "title": "服务器地址", + "tooltip": "配置IP地址和端口,如为https则需要增加https://前缀", + "type": "text", + "placeholder": "http://127.0.0.1:8096" + }, + "api_key": { + "id": "jellyfin.api_key", + "required": True, + "title": "Api Key", + "tooltip": "在Jellyfin设置->高级->API密钥处生成", + "type": "text", + "placeholder": "" + } + } + }, + "plex": { + "name": "Plex", + "img_url": "../static/img/plex.png", + "background": "bg-yellow", + "test_command": "app.mediaserver.client.plex|Plex", + "config": { + "host": { + "id": "plex.host", + "required": True, + "title": "服务器地址", + "tooltip": "配置IP地址和端口,如为https则需要增加https://前缀", + "type": "text", + "placeholder": "http://127.0.0.1:32400" + }, + "token": { + "id": "plex.token", + "required": False, + "title": "X-Plex-Token", + "tooltip": "Plex网页Cookie中的X-Plex-Token,通过浏览器F12->网络中获取,如填写将优先使用;Token与服务器名称、用户名及密码 二选一,推荐使用Token,连接速度更快", + "type": "text", + "placeholder": "X-Plex-Token与其它认证信息二选一" + }, + "servername": { + "id": "plex.servername", + "required": False, + "title": "服务器名称", + "tooltip": "配置Plex设置->左侧下拉框中看到的服务器名称;如填写了Token则无需填写服务器名称、用户名及密码", + "type": "text", + "placeholder": "" + }, + "username": { + "id": "plex.username", + "required": False, + "title": "用户名", + "type": "text", + "placeholder": "" + }, + "password": { + "id": "plex.password", + "required": False, + "title": "密码", + "type": "password", + "placeholder": "" + } + } + }, + } + + # 索引器 + INDEXER_CONF = {} + + # 发现过滤器 + DISCOVER_FILTER_CONF = { + "tmdb_movie": { + "with_genres": { + "name": "类型", + "type": "dropdown", + "options": [{'value': '', 'name': '全部'}, + {'value': '12', 'name': '冒险'}, + {'value': '16', 'name': '动画'}, + {'value': '35', 'name': '喜剧'}, + {'value': '80', 'name': '犯罪'}, + {'value': '18', 'name': '剧情'}, + {'value': '14', 'name': '奇幻'}, + {'value': '27', 'name': '恐怖'}, + {'value': '9648', 'name': '悬疑'}, + {'value': '10749', 'name': '爱情'}, + {'value': '878', 'name': '科幻'}, + {'value': '53', 'name': '惊悚'}, + {'value': '10752', 'name': '战争'}] + }, + "with_original_language": { + "name": "语言", + "type": "dropdown", + "options": [{'value': '', 'name': '全部'}, + {'value': 'zh', 'name': '中文'}, + {'value': 'en', 'name': '英语'}, + {'value': 'ja', 'name': '日语'}, + {'value': 'ko', 'name': '韩语'}, + {'value': 'fr', 'name': '法语'}, + {'value': 'de', 'name': '德语'}, + {'value': 'ru', 'name': '俄语'}, + {'value': 'hi', 'name': '印地语'}] + } + }, + "tmdb_tv": { + "with_genres": { + "name": "类型", + "type": "dropdown", + "options": [{'value': '', 'name': '全部'}, + {'value': '10759', 'name': '动作冒险'}, + {'value': '16', 'name': '动画'}, + {'value': '35', 'name': '喜剧'}, + {'value': '80', 'name': '犯罪'}, + {'value': '99', 'name': '纪录'}, + {'value': '18', 'name': '剧情'}, + {'value': '10762', 'name': '儿童'}, + {'value': '9648', 'name': '悬疑'}, + {'value': '10764', 'name': '真人秀'}, + {'value': '10765', 'name': '科幻'}] + }, + "with_original_language": { + "name": "语言", + "type": "dropdown", + "options": [{'value': '', 'name': '全部'}, + {'value': 'zh', 'name': '中文'}, + {'value': 'en', 'name': '英语'}, + {'value': 'ja', 'name': '日语'}, + {'value': 'ko', 'name': '韩语'}, + {'value': 'fr', 'name': '法语'}, + {'value': 'de', 'name': '德语'}, + {'value': 'ru', 'name': '俄语'}, + {'value': 'hi', 'name': '印地语'}] + } + }, + "douban_movie": { + "sort": { + "name": "排序", + "type": "dropdown", + "options": [{'value': '', 'name': '默认'}, + {'value': 'U', 'name': '综合排序'}, + {'value': 'T', 'name': '首播时间'}, + {'value': 'S', 'name': '高分优先'}, + {'value': 'R', 'name': '近期热度'}] + }, + "tags": { + "name": "类型", + "type": "dropdown", + "options": [{"value": "", "name": "全部"}, + {"value": "喜剧", "name": "喜剧"}, + {"value": "爱情", "name": "爱情"}, + {"value": "动作", "name": "动作"}, + {"value": "科幻", "name": "科幻"}, + {"value": "动画", "name": "动画"}, + {"value": "悬疑", "name": "悬疑"}, + {"value": "犯罪", "name": "犯罪"}, + {"value": "惊悚", "name": "惊悚"}, + {"value": "冒险", "name": "冒险"}, + {"value": "奇幻", "name": "奇幻"}, + {"value": "恐怖", "name": "恐怖"}, + {"value": "战争", "name": "战争"}, + {"value": "武侠", "name": "武侠"}, + {"value": "灾难", "name": "灾难"}] + } + }, + "douban_tv": { + "sort": { + "name": "排序", + "type": "dropdown", + "options": [{'value': '', 'name': '默认'}, + {'value': 'U', 'name': '综合排序'}, + {'value': 'T', 'name': '首播时间'}, + {'value': 'S', 'name': '高分优先'}, + {'value': 'R', 'name': '近期热度'}] + }, + "tags": { + "name": "地区", + "type": "dropdown", + "options": [{"value": "", "name": "全部"}, + {"value": "华语", "name": "华语"}, + {"value": "中国大陆", "name": "中国大陆"}, + {"value": "中国香港", "name": "中国香港"}, + {"value": "中国台湾", "name": "中国台湾"}, + {"value": "欧美", "name": "欧美"}, + {"value": "韩国", "name": "韩国"}, + {"value": "日本", "name": "日本"}, + {"value": "印度", "name": "印度"}, + {"value": "泰国", "name": "泰国"}] + } + } + } + + @staticmethod + def get_enum_name(enum, value): + """ + 根据Enum的value查询name + :param enum: 枚举 + :param value: 枚举值 + :return: 枚举名或None + """ + for e in enum: + if e.value == value: + return e.name + return None + + @staticmethod + def get_enum_item(enum, value): + """ + 根据Enum的value查询name + :param enum: 枚举 + :param value: 枚举值 + :return: 枚举项 + """ + for e in enum: + if e.value == value: + return e + return None + + @staticmethod + def get_dictenum_key(dictenum, value): + """ + 根据Enum dict的value查询key + :param dictenum: 枚举字典 + :param value: 枚举类(字典值)的值 + :return: 字典键或None + """ + for k, v in dictenum.items(): + if v.value == value: + return k + return None diff --git a/app/conf/siteconf.py b/app/conf/siteconf.py new file mode 100644 index 0000000..a069b55 --- /dev/null +++ b/app/conf/siteconf.py @@ -0,0 +1,480 @@ +class SiteConf: + + # 站点签到支持的识别XPATH + SITE_CHECKIN_XPATH = [ + '//a[@id="signed"]', + '//a[contains(@href, "attendance")]', + '//a[contains(text(), "签到")]', + '//a/b[contains(text(), "签 到")]', + '//span[@id="sign_in"]/a', + '//a[contains(@href, "addbonus")]', + '//input[@class="dt_button"][contains(@value, "打卡")]', + '//a[contains(@href, "sign_in")]', + '//a[contains(@onclick, "do_signin")]', + '//a[@id="do-attendance"]' + ] + + # 站点详情页字幕下载链接识别XPATH + SITE_SUBTITLE_XPATH = [ + '//td[@class="rowhead"][text()="字幕"]/following-sibling::td//a/@href', + ] + + # 站点登录界面元素XPATH + SITE_LOGIN_XPATH = { + "username": [ + '//input[@name="username"]', + '//input[@id="form_item_username"]' + ], + "password": [ + '//input[@name="password"]', + '//input[@id="form_item_password"]' + ], + "captcha": [ + '//input[@name="imagestring"]', + '//input[@name="captcha"]', + '//input[@id="form_item_captcha"]' + ], + "captcha_img": [ + '//img[@alt="CAPTCHA"]/@src', + '//img[@alt="SECURITY CODE"]/@src', + '//img[@id="LAY-user-get-vercode"]/@src', + '//img[contains(@src,"/api/getCaptcha")]/@src' + ], + "submit": [ + '//input[@type="submit"]', + '//button[@type="submit"]', + '//button[@lay-filter="login"]', + '//button[@lay-filter="formLogin"]', + '//input[@type="button"][@value="登录"]' + ], + "error": [ + "//table[@class='main']//td[@class='text']/text()" + ], + "twostep": [ + '//input[@name="two_step_code"]', + '//input[@name="2fa_secret"]' + ] + } + + # 检测种子促销的站点XPATH,不在此清单的无法开启仅RSS免费种子功能 + RSS_SITE_GRAP_CONF = { + 'jptv.club': { + 'FREE': ["//span/i[@class='fas fa-star text-gold']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//span[@class='badge-extra text-green']"], + }, + 'pthome.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'ptsbao.club': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'totheglory.im': { + 'FREE': ["//img[@class='topic'][contains(@src,'ico_free.gif')]"], + '2XFREE': [], + 'HR': ["//img[@src='/pic/hit_run.gif']"], + 'PEER_COUNT': ["//span[@id='dlstatus']"], + }, + 'www.beitai.pt': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdtime.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'www.haidan.video': { + 'FREE': ["//img[@class='pro_free'][@title='免费']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//div[@class='torrent']/div[1]/div[1]/div[3]"], + }, + 'kp.m-team.cc': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'lemonhd.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"] + }, + 'discfan.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.sjtu.edu.cn': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'nanyangpt.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'audiences.me': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"] + }, + 'pterclub.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["(//td[@align='left' and @class='rowfollow' and @valign='top']/b[1])[3]"] + }, + 'et8.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'pt.keepfrds.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'www.pttime.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']", "//h1[@id='top']/b/font[@class='zeroupzerodown']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + '1ptba.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'www.tjupt.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//font[@class='twoup'][text()='2X']"], + 'HR': ["//font[@color='red'][text()='Hit&Run']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdhome.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdsky.me': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hdcity.city': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hdcity.leniter.org': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hdcity.work': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hdcity4.leniter.org': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'open.cd': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': ["//img[@class='pro_free2up']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'ourbits.club': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.btschool.club': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.eastgame.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.soulvoice.club': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': ["//img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'springsunday.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'www.htpt.cc': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'chdbits.co': { + 'FREE': ["//h1[@id='top']/img[@class='pro_free']"], + '2XFREE': [], + 'HR': ["//b[contains(text(),'H&R:')]"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdchina.org': { + 'RENDER': True, + 'FREE': ["//h2[@id='top']/img[@class='pro_free']"], + '2XFREE': ["//h2[@id='top']/img[@class='pro_free2up']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "ccfbits.org": { + 'FREE': ["//font[@color='red'][text()='本种子不计下载量,只计上传量!']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'u2.dmhy.org': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'www.hdarea.co': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdatmos.club': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'avgv.cc': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hdfans.org': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdpt.xyz': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'azusa.ru': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdmayi.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdzone.me': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'gainbound.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'hdvideo.one': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + '52pt.site': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.msg.vg': { + 'LOGIN': 'user/login/index', + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'kamept.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'carpt.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'club.hares.top': { + 'FREE': ["//b[@class='free'][text()='免费']"], + '2XFREE': ["//b[@class='twoupfree'][text()='2X免费']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'www.hddolby.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'piggo.me': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'pt.0ff.cc': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'wintersakura.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'pt.hdupt.com': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'pt.upxin.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'www.nicept.net': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'ptchina.org': { + 'FREE': ["//h1[@id='top']/b/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'www.hd.ai': { + 'FREE': ["//img[@class='pro_free']"], + '2XFREE': [], + 'HR': [], + 'PEER_COUNT': [], + }, + 'hhanclub.top': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': [], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'zmpt.cc': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + 'ihdbits.me': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': [], + }, + 'leaves.red': { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "sharkpt.net": { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': ["//h1[@id='top']/img[@class='hitandrun']"], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "pt.2xfree.org": { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "uploads.ltd": { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "www.icc2022.com": { + 'FREE': ["//h1[@id='top']/b/font[@class='free']"], + '2XFREE': ["//h1[@id='top']/b/font[@class='twoupfree']"], + 'HR': [], + 'PEER_COUNT': ["//div[@id='peercount']/b[1]"], + }, + "zhuque.in": { + 'RENDER': True, + 'FREE': ["//span[@class='text-download'][contains(text(),'0x')]"], + '2XFREE': [""], + 'HR': [], + 'PEER_COUNT': ["//div[@class='ant-form-item-control-input-content']/span[contains(text(),'正在做种: )]"], + } + } + # 公共BT站点 + PUBLIC_TORRENT_SITES = {} diff --git a/app/conf/systemconfig.py b/app/conf/systemconfig.py new file mode 100644 index 0000000..fc3c2fd --- /dev/null +++ b/app/conf/systemconfig.py @@ -0,0 +1,67 @@ +import json + +from app.helper import DictHelper +from app.utils.commons import singleton + + +@singleton +class SystemConfig: + + # 系统设置 + systemconfig = { + # 默认下载设置 + "DefaultDownloadSetting": None, + # CookieCloud的设置 + "CookieCloud": {}, + # 自动获取Cookie的用户信息 + "CookieUserInfo": {}, + # 用户自定义CSS/JavsScript + "CustomScript": {}, + # 播放限速设置 + "SpeedLimit": {} + } + + def __init__(self): + self.init_config() + + def init_config(self, key=None): + """ + 缓存系统设置 + """ + def __set_value(_key, _value): + if isinstance(_value, dict) \ + or isinstance(_value, list): + dict_value = DictHelper().get("SystemConfig", _key) + if dict_value: + self.systemconfig[_key] = json.loads(dict_value) + else: + self.systemconfig[_key] = {} + else: + self.systemconfig[_key] = DictHelper().get("SystemConfig", _key) + + if key: + __set_value(key, self.systemconfig.get(key)) + else: + for key, value in self.systemconfig.items(): + __set_value(key, value) + + def set_system_config(self, key, value): + """ + 设置系统设置 + """ + if isinstance(value, dict) \ + or isinstance(value, list): + if value: + value = json.dumps(value) + else: + value = None + DictHelper().set("SystemConfig", key, value) + self.init_config(key) + + def get_system_config(self, key=None): + """ + 获取系统设置 + """ + if not key: + return self.systemconfig + return self.systemconfig.get(key) diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..cfe8c5d --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1,44 @@ +import os +import log +from config import Config +from .main_db import MainDb +from .main_db import DbPersist +from .media_db import MediaDb +from alembic.config import Config as AlembicConfig +from alembic.command import upgrade as alembic_upgrade + + +def init_db(): + """ + 初始化数据库 + """ + log.console('开始初始化数据库...') + MediaDb().init_db() + MainDb().init_db() + log.console('数据库初始化完成') + + +def init_data(): + """ + 初始化数据 + """ + log.console('开始初始化数据...') + MainDb().init_data() + log.console('数据初始化完成') + + +def update_db(): + """ + 更新数据库 + """ + db_location = os.path.join(Config().get_config_path(), 'user.db') + script_location = os.path.join(Config().get_root_path(), 'db_scripts') + log.console('开始更新数据库...') + try: + alembic_cfg = AlembicConfig() + alembic_cfg.set_main_option('script_location', script_location) + alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") + alembic_upgrade(alembic_cfg, 'head') + except Exception as e: + print(str(e)) + log.console('数据库更新完成') diff --git a/app/db/main_db.py b/app/db/main_db.py new file mode 100644 index 0000000..3620818 --- /dev/null +++ b/app/db/main_db.py @@ -0,0 +1,122 @@ +import os +import threading +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.pool import QueuePool + +from app.db.models import Base +from app.utils import ExceptionUtils, PathUtils +from config import Config + +lock = threading.Lock() +_Engine = create_engine( + f"sqlite:///{os.path.join(Config().get_config_path(), 'user.db')}?check_same_thread=False", + echo=False, + poolclass=QueuePool, + pool_pre_ping=True, + pool_size=50, + pool_recycle=60 * 10, + max_overflow=0 +) +_Session = scoped_session(sessionmaker(bind=_Engine, + autoflush=True, + autocommit=False, + expire_on_commit=False)) + + +class MainDb: + + @property + def session(self): + return _Session() + + @staticmethod + def init_db(): + with lock: + Base.metadata.create_all(_Engine) + + def init_data(self): + """ + 读取config目录下的sql文件,并初始化到数据库,只处理一次 + """ + config = Config().get_config() + init_files = Config().get_config("app").get("init_files") or [] + config_dir = Config().get_script_path() + sql_files = PathUtils.get_dir_level1_files(in_path=config_dir, exts=".sql") + config_flag = False + for sql_file in sql_files: + if os.path.basename(sql_file) not in init_files: + config_flag = True + with open(sql_file, "r", encoding="utf-8") as f: + sql_list = f.read().split(';\n') + for sql in sql_list: + try: + self.excute(sql) + self.commit() + except Exception as err: + print(str(err)) + init_files.append(os.path.basename(sql_file)) + if config_flag: + config['app']['init_files'] = init_files + Config().save_config(config) + + def insert(self, data): + """ + 插入数据 + """ + if isinstance(data, list): + self.session.add_all(data) + else: + self.session.add(data) + + def query(self, *obj): + """ + 查询对象 + """ + return self.session.query(*obj) + + def excute(self, sql): + """ + 执行SQL语句 + """ + self.session.execute(sql) + + def flush(self): + """ + 刷写 + """ + self.session.flush() + + def commit(self): + """ + 提交事务 + """ + self.session.commit() + + def rollback(self): + """ + 回滚事务 + """ + self.session.rollback() + + +class DbPersist(object): + """ + 数据库持久化装饰器 + """ + + def __init__(self, db): + self.db = db + + def __call__(self, f): + def persist(*args, **kwargs): + try: + ret = f(*args, **kwargs) + self.db.commit() + return True if ret is None else ret + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.db.rollback() + return False + + return persist diff --git a/app/db/media_db.py b/app/db/media_db.py new file mode 100644 index 0000000..4e22eaf --- /dev/null +++ b/app/db/media_db.py @@ -0,0 +1,126 @@ +import os +import threading +import time +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.pool import QueuePool +from app.db.models import BaseMedia, MEDIASYNCITEMS, MEDIASYNCSTATISTIC +from app.utils import ExceptionUtils +from config import Config + +lock = threading.Lock() +_Engine = create_engine( + f"sqlite:///{os.path.join(Config().get_config_path(), 'media.db')}?check_same_thread=False", + echo=False, + poolclass=QueuePool, + pool_pre_ping=True, + pool_size=50, + pool_recycle=60 * 10, + max_overflow=0 +) +_Session = scoped_session(sessionmaker(bind=_Engine, + autoflush=True, + autocommit=False)) + + +class MediaDb: + + @property + def session(self): + return _Session() + + @staticmethod + def init_db(): + with lock: + BaseMedia.metadata.create_all(_Engine) + + def insert(self, server_type, iteminfo): + if not server_type or not iteminfo: + return False + try: + self.session.query(MEDIASYNCITEMS).filter(MEDIASYNCITEMS.SERVER == server_type, + MEDIASYNCITEMS.ITEM_ID == iteminfo.get("id")).delete() + self.session.flush() + self.session.add(MEDIASYNCITEMS( + SERVER=server_type, + LIBRARY=iteminfo.get("library"), + ITEM_ID=iteminfo.get("id"), + ITEM_TYPE=iteminfo.get("type"), + TITLE=iteminfo.get("title"), + ORGIN_TITLE=iteminfo.get("originalTitle"), + YEAR=iteminfo.get("year"), + TMDBID=iteminfo.get("tmdbid"), + IMDBID=iteminfo.get("imdbid"), + PATH=iteminfo.get("path") + )) + self.session.commit() + return True + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.session.rollback() + return False + + def empty(self, server_type=None, library=None): + try: + if server_type and library: + self.session.query(MEDIASYNCITEMS).filter(MEDIASYNCITEMS.SERVER == server_type, + MEDIASYNCITEMS.LIBRARY == library).delete() + else: + self.session.query(MEDIASYNCITEMS).delete() + self.session.commit() + return True + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.session.rollback() + return False + + def statistics(self, server_type, total_count, movie_count, tv_count): + if not server_type: + return False + try: + self.session.query(MEDIASYNCSTATISTIC).filter(MEDIASYNCSTATISTIC.SERVER == server_type).delete() + self.session.flush() + self.session.add(MEDIASYNCSTATISTIC( + SERVER=server_type, + TOTAL_COUNT=total_count, + MOVIE_COUNT=movie_count, + TV_COUNT=tv_count, + UPDATE_TIME=time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(time.time())) + )) + self.session.commit() + return True + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.session.rollback() + return False + + def exists(self, server_type, title, year, tmdbid): + if not server_type or not title: + return False + if tmdbid: + count = self.session.query(MEDIASYNCITEMS).filter(MEDIASYNCITEMS.TMDBID == str(tmdbid)).count() + if count: + return True + if year: + items = self.session.query(MEDIASYNCITEMS).filter(MEDIASYNCITEMS.SERVER == server_type, + MEDIASYNCITEMS.TITLE == title, + MEDIASYNCITEMS.YEAR == str(year)).all() + else: + items = self.session.query(MEDIASYNCITEMS).filter(MEDIASYNCITEMS.SERVER == server_type, + MEDIASYNCITEMS.TITLE == title).all() + if items: + if tmdbid: + for item in items: + if not item.TMDBID or item.TMDBID == str(tmdbid): + return True + return False + else: + return True + else: + return False + + def get_statistics(self, server_type): + if not server_type: + return None + return self.session.query(MEDIASYNCSTATISTIC).filter(MEDIASYNCSTATISTIC.SERVER == server_type).first() diff --git a/app/db/models.py b/app/db/models.py new file mode 100644 index 0000000..aa0f0ef --- /dev/null +++ b/app/db/models.py @@ -0,0 +1,578 @@ +# coding: utf-8 +from sqlalchemy import Column, Float, Index, Integer, Text, text, Sequence +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +BaseMedia = declarative_base() + + +class CONFIGFILTERGROUP(Base): + __tablename__ = 'CONFIG_FILTER_GROUP' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + GROUP_NAME = Column(Text) + IS_DEFAULT = Column(Text) + NOTE = Column(Text) + + +class CONFIGFILTERRULES(Base): + __tablename__ = 'CONFIG_FILTER_RULES' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + GROUP_ID = Column(Text, index=True) + ROLE_NAME = Column(Text) + PRIORITY = Column(Text) + INCLUDE = Column(Text) + EXCLUDE = Column(Text) + SIZE_LIMIT = Column(Text) + NOTE = Column(Text) + + +class CONFIGRSSPARSER(Base): + __tablename__ = 'CONFIG_RSS_PARSER' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + TYPE = Column(Text) + FORMAT = Column(Text) + PARAMS = Column(Text) + NOTE = Column(Text) + SYSDEF = Column(Text) + + +class CONFIGSITE(Base): + __tablename__ = 'CONFIG_SITE' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + PRI = Column(Text) + RSSURL = Column(Text) + SIGNURL = Column(Text) + COOKIE = Column(Text) + INCLUDE = Column(Text) + EXCLUDE = Column(Text) + SIZE = Column(Text) + NOTE = Column(Text) + + +class CONFIGSYNCPATHS(Base): + __tablename__ = 'CONFIG_SYNC_PATHS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SOURCE = Column(Text) + DEST = Column(Text) + UNKNOWN = Column(Text) + MODE = Column(Text) + RENAME = Column(Integer) + ENABLED = Column(Integer) + NOTE = Column(Text) + + +class CONFIGUSERS(Base): + __tablename__ = 'CONFIG_USERS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + PASSWORD = Column(Text) + PRIS = Column(Text) + + +class CONFIGUSERRSS(Base): + __tablename__ = 'CONFIG_USER_RSS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + ADDRESS = Column(Text) + PARSER = Column(Text) + INTERVAL = Column(Text) + USES = Column(Text) + INCLUDE = Column(Text) + EXCLUDE = Column(Text) + FILTER = Column(Text) + UPDATE_TIME = Column(Text) + PROCESS_COUNT = Column(Text) + STATE = Column(Text) + SAVE_PATH = Column(Text) + DOWNLOAD_SETTING = Column(Integer) + RECOGNIZATION = Column(Text) + OVER_EDITION = Column(Integer) + SITES = Column(Text) + FILTER_ARGS = Column(Text) + MEDIAINFOS = Column(Text) + NOTE = Column(Text) + + +class CUSTOMWORDS(Base): + __tablename__ = 'CUSTOM_WORDS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + REPLACED = Column(Text) + REPLACE = Column(Text) + FRONT = Column(Text) + BACK = Column(Text) + OFFSET = Column(Text) + TYPE = Column(Integer) + GROUP_ID = Column(Integer) + SEASON = Column(Integer) + ENABLED = Column(Integer) + REGEX = Column(Integer) + HELP = Column(Text) + NOTE = Column(Text) + + +class CUSTOMWORDGROUPS(Base): + __tablename__ = 'CUSTOM_WORD_GROUPS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TITLE = Column(Text) + YEAR = Column(Text) + TYPE = Column(Integer) + TMDBID = Column(Integer) + SEASON_COUNT = Column(Integer) + NOTE = Column(Text) + + +class DOUBANMEDIAS(Base): + __tablename__ = 'DOUBAN_MEDIAS' + __table_args__ = ( + Index('INDX_DOUBAN_MEDIAS_NAME', 'NAME', 'YEAR'), + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + YEAR = Column(Text) + TYPE = Column(Text) + RATING = Column(Text) + IMAGE = Column(Text) + STATE = Column(Text) + ADD_TIME = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class DOWNLOADHISTORY(Base): + __tablename__ = 'DOWNLOAD_HISTORY' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TITLE = Column(Text, index=True) + YEAR = Column(Text) + TYPE = Column(Text) + TMDBID = Column(Text) + VOTE = Column(Text) + POSTER = Column(Text) + OVERVIEW = Column(Text) + TORRENT = Column(Text) + ENCLOSURE = Column(Text) + SITE = Column(Text) + DESC = Column(Text) + DATE = Column(Text, index=True) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class DOWNLOADSETTING(Base): + __tablename__ = 'DOWNLOAD_SETTING' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + CATEGORY = Column(Text) + TAGS = Column(Text) + CONTENT_LAYOUT = Column(Integer) + IS_PAUSED = Column(Integer) + UPLOAD_LIMIT = Column(Integer) + DOWNLOAD_LIMIT = Column(Integer) + RATIO_LIMIT = Column(Integer) + SEEDING_TIME_LIMIT = Column(Integer) + DOWNLOADER = Column(Text) + NOTE = Column(Text) + + +class MESSAGECLIENT(Base): + __tablename__ = 'MESSAGE_CLIENT' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + TYPE = Column(Text) + CONFIG = Column(Text) + SWITCHS = Column(Text) + INTERACTIVE = Column(Integer) + ENABLED = Column(Integer) + NOTE = Column(Text) + + +class RSSHISTORY(Base): + __tablename__ = 'RSS_HISTORY' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TYPE = Column(Text) + RSSID = Column(Text, index=True) + NAME = Column(Text) + YEAR = Column(Text) + TMDBID = Column(Text) + SEASON = Column(Text) + IMAGE = Column(Text) + DESC = Column(Text) + TOTAL = Column(Integer) + START = Column(Integer) + FINISH_TIME = Column(Text) + NOTE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class RSSMOVIES(Base): + __tablename__ = 'RSS_MOVIES' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + YEAR = Column(Text) + KEYWORD = Column(Text) + TMDBID = Column(Text) + IMAGE = Column(Text) + RSS_SITES = Column(Text) + SEARCH_SITES = Column(Text) + OVER_EDITION = Column(Integer) + FILTER_ORDER = Column(Integer) + FILTER_RESTYPE = Column(Text) + FILTER_PIX = Column(Text) + FILTER_RULE = Column(Integer) + FILTER_TEAM = Column(Text) + SAVE_PATH = Column(Text) + DOWNLOAD_SETTING = Column(Integer) + FUZZY_MATCH = Column(Integer) + STATE = Column(Text) + DESC = Column(Text) + NOTE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class RSSTORRENTS(Base): + __tablename__ = 'RSS_TORRENTS' + __table_args__ = ( + Index('INDX_RSS_TORRENTS_NAME', 'TITLE', 'YEAR', 'SEASON', 'EPISODE'), + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TORRENT_NAME = Column(Text) + ENCLOSURE = Column(Text, index=True) + TYPE = Column(Text) + TITLE = Column(Text) + YEAR = Column(Text) + SEASON = Column(Text) + EPISODE = Column(Text) + + +class RSSTVS(Base): + __tablename__ = 'RSS_TVS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + YEAR = Column(Text) + KEYWORD = Column(Text) + SEASON = Column(Text) + TMDBID = Column(Text) + IMAGE = Column(Text) + RSS_SITES = Column(Text) + SEARCH_SITES = Column(Text) + OVER_EDITION = Column(Integer) + FILTER_ORDER = Column(Integer) + FILTER_RESTYPE = Column(Text) + FILTER_PIX = Column(Text) + FILTER_RULE = Column(Integer) + FILTER_TEAM = Column(Text) + SAVE_PATH = Column(Text) + DOWNLOAD_SETTING = Column(Integer) + FUZZY_MATCH = Column(Integer) + TOTAL_EP = Column(Integer) + CURRENT_EP = Column(Integer) + TOTAL = Column(Integer) + LACK = Column(Integer) + STATE = Column(Text) + DESC = Column(Text) + NOTE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class RSSTVEPISODES(Base): + __tablename__ = 'RSS_TV_EPISODES' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + RSSID = Column(Text, index=True) + EPISODES = Column(Text) + + +class TORRENTREMOVETASK(Base): + __tablename__ = 'TORRENT_REMOVE_TASK' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + ACTION = Column(Integer) + INTERVAL = Column(Integer) + ENABLED = Column(Integer) + SAMEDATA = Column(Integer) + ONLYNASTOOL = Column(Integer) + DOWNLOADER = Column(Text) + CONFIG = Column(Text) + NOTE = Column(Text) + + +class SEARCHRESULTINFO(Base): + __tablename__ = 'SEARCH_RESULT_INFO' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TORRENT_NAME = Column(Text) + ENCLOSURE = Column(Text) + DESCRIPTION = Column(Text) + TYPE = Column(Text) + TITLE = Column(Text) + YEAR = Column(Text) + SEASON = Column(Text) + EPISODE = Column(Text) + ES_STRING = Column(Text) + VOTE = Column(Text) + IMAGE = Column(Text) + POSTER = Column(Text) + TMDBID = Column(Text) + OVERVIEW = Column(Text) + RES_TYPE = Column(Text) + RES_ORDER = Column(Text) + SIZE = Column(Integer) + SEEDERS = Column(Integer) + PEERS = Column(Integer) + SITE = Column(Text) + SITE_ORDER = Column(Text) + PAGEURL = Column(Text) + OTHERINFO = Column(Text) + UPLOAD_VOLUME_FACTOR = Column(Float) + DOWNLOAD_VOLUME_FACTOR = Column(Float) + NOTE = Column(Text) + + +class SITEBRUSHDOWNLOADERS(Base): + __tablename__ = 'SITE_BRUSH_DOWNLOADERS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text) + TYPE = Column(Text) + HOST = Column(Text) + PORT = Column(Text) + USERNAME = Column(Text) + PASSWORD = Column(Text) + SAVE_DIR = Column(Text) + NOTE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class SITEBRUSHTASK(Base): + __tablename__ = 'SITE_BRUSH_TASK' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + NAME = Column(Text, index=True) + SITE = Column(Text) + FREELEECH = Column(Text) + RSS_RULE = Column(Text) + REMOVE_RULE = Column(Text) + SEED_SIZE = Column(Text) + INTEVAL = Column(Text) + DOWNLOADER = Column(Text) + TRANSFER = Column(Text) + DOWNLOAD_COUNT = Column(Text) + REMOVE_COUNT = Column(Text) + DOWNLOAD_SIZE = Column(Text) + UPLOAD_SIZE = Column(Text) + SENDMESSAGE = Column(Text) + FORCEUPLOAD = Column(Text) + STATE = Column(Text) + LST_MOD_DATE = Column(Text) + + +class SITEBRUSHTORRENTS(Base): + __tablename__ = 'SITE_BRUSH_TORRENTS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TASK_ID = Column(Text, index=True) + TORRENT_NAME = Column(Text) + TORRENT_SIZE = Column(Text) + ENCLOSURE = Column(Text) + DOWNLOADER = Column(Text) + DOWNLOAD_ID = Column(Text) + LST_MOD_DATE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class SITESTATISTICSHISTORY(Base): + __tablename__ = 'SITE_STATISTICS_HISTORY' + __table_args__ = ( + Index('INDX_SITE_STATISTICS_HISTORY_DS', 'DATE', 'URL'), + Index('UN_INDX_SITE_STATISTICS_HISTORY_DS', 'DATE', 'URL', unique=True) + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SITE = Column(Text) + DATE = Column(Text) + USER_LEVEL = Column(Text) + UPLOAD = Column(Text) + DOWNLOAD = Column(Text) + RATIO = Column(Text) + SEEDING = Column(Integer, server_default=text("0")) + LEECHING = Column(Integer, server_default=text("0")) + SEEDING_SIZE = Column(Integer, server_default=text("0")) + BONUS = Column(Float, server_default=text("0.0")) + URL = Column(Text) + + +class SITEUSERINFOSTATS(Base): + __tablename__ = 'SITE_USER_INFO_STATS' + __table_args__ = ( + Index('INDX_SITE_USER_INFO_STATS_URL', 'URL'), + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SITE = Column(Text, index=True) + USERNAME = Column(Text) + USER_LEVEL = Column(Text) + JOIN_AT = Column(Text) + UPDATE_AT = Column(Text) + UPLOAD = Column(Integer) + DOWNLOAD = Column(Integer) + RATIO = Column(Float) + SEEDING = Column(Integer) + LEECHING = Column(Integer) + SEEDING_SIZE = Column(Integer) + BONUS = Column(Float) + URL = Column(Text, unique=True) + MSG_UNREAD = Column(Integer) + EXT_INFO = Column(Text) + + +class SITEFAVICON(Base): + __tablename__ = 'SITE_FAVICON' + + SITE = Column(Text, primary_key=True) + URL = Column(Text) + FAVICON = Column(Text) + + +class SITEUSERSEEDINGINFO(Base): + __tablename__ = 'SITE_USER_SEEDING_INFO' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SITE = Column(Text, index=True) + SEEDING_INFO = Column(Text, server_default=text("'[]'")) + UPDATE_AT = Column(Text) + URL = Column(Text, unique=True) + + +class SYNCHISTORY(Base): + __tablename__ = 'SYNC_HISTORY' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + PATH = Column(Text, index=True) + SRC = Column(Text) + DEST = Column(Text) + + +class SYSTEMDICT(Base): + __tablename__ = 'SYSTEM_DICT' + __table_args__ = ( + Index('INDX_SYSTEM_DICT', 'TYPE', 'KEY'), + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TYPE = Column(Text) + KEY = Column(Text) + VALUE = Column(Text) + NOTE = Column(Text) + + +class TRANSFERBLACKLIST(Base): + __tablename__ = 'TRANSFER_BLACKLIST' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + PATH = Column(Text, index=True) + + +class TRANSFERHISTORY(Base): + __tablename__ = 'TRANSFER_HISTORY' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + MODE = Column(Text) + TYPE = Column(Text) + CATEGORY = Column(Text) + TMDBID = Column(Integer) + TITLE = Column(Text, index=True) + YEAR = Column(Text) + SEASON_EPISODE = Column(Text) + SOURCE = Column(Text) + SOURCE_PATH = Column(Text, index=True) + SOURCE_FILENAME = Column(Text, index=True) + DEST = Column(Text) + DEST_PATH = Column(Text) + DEST_FILENAME = Column(Text) + DATE = Column(Text) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class TRANSFERUNKNOWN(Base): + __tablename__ = 'TRANSFER_UNKNOWN' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + PATH = Column(Text, index=True) + DEST = Column(Text) + MODE = Column(Text) + STATE = Column(Text, index=True) + + +class USERRSSTASKHISTORY(Base): + __tablename__ = 'USERRSS_TASK_HISTORY' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + TASK_ID = Column(Text, index=True) + TITLE = Column(Text) + DOWNLOADER = Column(Text) + DATE = Column(Text) + + +class MEDIASYNCITEMS(BaseMedia): + __tablename__ = 'MEDIASYNC_ITEMS' + __table_args__ = ( + Index('INDX_MEDIASYNC_ITEMS_SL', 'SERVER', 'LIBRARY'), + ) + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SERVER = Column(Text) + LIBRARY = Column(Text) + ITEM_ID = Column(Text, index=True) + ITEM_TYPE = Column(Text) + TITLE = Column(Text, index=True) + ORGIN_TITLE = Column(Text, index=True) + YEAR = Column(Text) + TMDBID = Column(Text, index=True) + IMDBID = Column(Text) + PATH = Column(Text) + NOTE = Column(Text) + JSON = Column(Text) + + +class MEDIASYNCSTATISTIC(BaseMedia): + __tablename__ = 'MEDIASYNC_STATISTICS' + + ID = Column(Integer, Sequence('ID'), primary_key=True) + SERVER = Column(Text, index=True) + TOTAL_COUNT = Column(Text) + MOVIE_COUNT = Column(Text) + TV_COUNT = Column(Text) + UPDATE_TIME = Column(Text) diff --git a/app/doubansync.py b/app/doubansync.py new file mode 100644 index 0000000..edf0857 --- /dev/null +++ b/app/doubansync.py @@ -0,0 +1,286 @@ +import datetime +import random +from threading import Lock +from time import sleep + +import log +from app.downloader import Downloader +from app.helper import DbHelper +from app.media import Media, DouBan +from app.media.meta import MetaInfo +from app.message import Message +from app.searcher import Searcher +from app.subscribe import Subscribe +from app.utils import ExceptionUtils +from app.utils.types import SearchType, MediaType +from config import Config + +lock = Lock() + + +class DoubanSync: + douban = None + searcher = None + media = None + downloader = None + dbhelper = None + subscribe = None + _interval = None + _auto_search = None + _auto_rss = None + _users = None + _days = None + _types = None + + def __init__(self): + self.douban = DouBan() + self.searcher = Searcher() + self.downloader = Downloader() + self.media = Media() + self.message = Message() + self.dbhelper = DbHelper() + self.subscribe = Subscribe() + self.init_config() + + def init_config(self): + douban = Config().get_config('douban') + if douban: + # 同步间隔 + self._interval = int(douban.get('interval')) if str(douban.get('interval')).isdigit() else None + self._auto_search = douban.get('auto_search') + self._auto_rss = douban.get('auto_rss') + # 用户列表 + users = douban.get('users') + if users: + if not isinstance(users, list): + users = [users] + self._users = users + # 时间范围 + self._days = int(douban.get('days')) if str(douban.get('days')).isdigit() else None + # 类型 + types = douban.get('types') + if types: + self._types = types.split(',') + + def sync(self): + """ + 同步豆瓣数据 + """ + if not self._interval: + log.info("【Douban】豆瓣配置:同步间隔未配置或配置不正确") + return + with lock: + log.info("【Douban】开始同步豆瓣数据...") + # 拉取豆瓣数据 + medias = self.__get_all_douban_movies() + # 开始检索 + for media in medias: + if not media or not media.get_name(): + continue + try: + # 查询数据库状态,已经加入RSS的不处理 + search_state = self.dbhelper.get_douban_search_state(media.get_name(), media.year) + if not search_state or search_state[0] == "NEW": + if self._auto_search: + # 需要检索 + if media.begin_season: + subtitle = "第%s季" % media.begin_season + else: + subtitle = None + media_info = self.media.get_media_info(title="%s %s" % (media.get_name(), media.year or ""), + subtitle=subtitle, + mtype=media.type) + # 不需要自动加订阅,则直接搜索 + if not media_info or not media_info.tmdb_info: + log.warn("【Douban】%s 未查询到媒体信息" % media.get_name()) + continue + # 检查是否存在,电视剧返回不存在的集清单 + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info) + # 已经存在 + if exist_flag: + # 更新为已下载状态 + log.info("【Douban】%s 已存在" % media.get_name()) + self.dbhelper.insert_douban_media_state(media, "DOWNLOADED") + continue + if not self._auto_rss: + # 合并季 + media_info.begin_season = media.begin_season + # 开始检索 + search_result, no_exists, search_count, download_count = self.searcher.search_one_media( + media_info=media_info, + in_from=SearchType.DB, + no_exists=no_exists, + user_name=media_info.user_name) + if search_result: + # 下载全了更新为已下载,没下载全的下次同步再次搜索 + self.dbhelper.insert_douban_media_state(media, "DOWNLOADED") + else: + # 需要加订阅,则由订阅去检索 + log.info( + "【Douban】%s %s 更新到%s订阅中..." % (media.get_name(), media.year, media.type.value)) + code, msg, _ = self.subscribe.add_rss_subscribe(mtype=media.type, + name=media.get_name(), + year=media.year, + season=media.begin_season, + mediaid=f"DB:{media.douban_id}") + if code != 0: + log.error("【Douban】%s 添加订阅失败:%s" % (media.get_name(), msg)) + # 订阅已存在 + if code == 9: + self.dbhelper.insert_douban_media_state(media, "RSS") + else: + # 发送订阅消息 + self.message.send_rss_success_message(in_from=SearchType.DB, + media_info=media) + # 插入为已RSS状态 + self.dbhelper.insert_douban_media_state(media, "RSS") + else: + # 不需要检索 + if self._auto_rss: + # 加入订阅,使状态为R + log.info("【Douban】%s %s 更新到%s订阅中..." % ( + media.get_name(), media.year, media.type.value)) + code, msg, _ = self.subscribe.add_rss_subscribe(mtype=media.type, + name=media.get_name(), + year=media.year, + season=media.begin_season, + mediaid=f"DB:{media.douban_id}", + state="R") + if code != 0: + log.error("【Douban】%s 添加订阅失败:%s" % (media.get_name(), msg)) + # 订阅已存在 + if code == 9: + self.dbhelper.insert_douban_media_state(media, "RSS") + else: + # 发送订阅消息 + self.message.send_rss_success_message(in_from=SearchType.DB, + media_info=media) + # 插入为已RSS状态 + self.dbhelper.insert_douban_media_state(media, "RSS") + elif not search_state: + log.info("【Douban】%s %s 更新到%s列表中..." % ( + media.get_name(), media.year, media.type.value)) + self.dbhelper.insert_douban_media_state(media, "NEW") + + else: + log.info("【Douban】%s %s 已处理过" % (media.get_name(), media.year)) + except Exception as err: + log.error("【Douban】%s %s 处理失败:%s" % (media.get_name(), media.year, str(err))) + continue + log.info("【Douban】豆瓣数据同步完成") + + def __get_all_douban_movies(self): + """ + 获取每一个用户的每一个类型的豆瓣标记 + :return: 检索到的媒体信息列表(不含TMDB信息) + """ + if not self._interval \ + or not self._days \ + or not self._users \ + or not self._types: + log.warn("【Douban】豆瓣未配置或配置不正确") + return [] + # 返回媒体列表 + media_list = [] + # 豆瓣ID列表 + douban_ids = {} + # 每页条数 + perpage_number = 15 + # 每一个用户 + for user in self._users: + if not user: + continue + # 查询用户名称 + user_name = "" + userinfo = self.douban.get_user_info(userid=user) + if userinfo: + user_name = userinfo.get("name") + # 每一个类型成功数量 + user_succnum = 0 + for mtype in self._types: + if not mtype: + continue + log.info(f"【Douban】开始获取 {user_name or user} 的 {mtype} 数据...") + # 开始序号 + start_number = 0 + # 类型成功数量 + user_type_succnum = 0 + # 每一页 + while True: + # 页数 + page_number = int(start_number / perpage_number + 1) + # 当前页成功数量 + sucess_urlnum = 0 + # 是否继续下一页 + continue_next_page = True + log.debug(f"【Douban】开始解析第 {page_number} 页数据...") + try: + items = self.douban.get_douban_wish(dtype=mtype, userid=user, start=start_number, wait=True) + if not items: + log.warn(f"【Douban】第 {page_number} 页未获取到数据") + break + # 解析豆瓣ID + for item in items: + # 时间范围 + date = item.get("date") + if not date: + continue_next_page = False + break + else: + mark_date = datetime.datetime.strptime(date, '%Y-%m-%d') + if not (datetime.datetime.now() - mark_date).days < int(self._days): + continue_next_page = False + break + doubanid = item.get("id") + if str(doubanid).isdigit(): + log.info("【Douban】解析到媒体:%s" % doubanid) + if doubanid not in douban_ids: + douban_ids[doubanid] = { + "user_name": user_name + } + sucess_urlnum += 1 + user_type_succnum += 1 + user_succnum += 1 + log.debug( + f"【Douban】{user_name or user} 第 {page_number} 页解析完成,共获取到 {sucess_urlnum} 个媒体") + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error(f"【Douban】{user_name or user} 第 {page_number} 页解析出错:%s" % str(err)) + break + # 继续下一页 + if continue_next_page: + start_number += perpage_number + else: + break + # 当前类型解析结束 + log.debug(f"【Douban】用户 {user_name or user} 的 {mtype} 解析完成,共获取到 {user_type_succnum} 个媒体") + log.info(f"【Douban】用户 {user_name or user} 解析完成,共获取到 {user_succnum} 个媒体") + log.info(f"【Douban】所有用户解析完成,共获取到 {len(douban_ids)} 个媒体") + # 查询豆瓣详情 + for doubanid, info in douban_ids.items(): + douban_info = self.douban.get_douban_detail(doubanid=doubanid, wait=True) + # 组装媒体信息 + if not douban_info: + log.warn("【Douban】%s 未正确获取豆瓣详细信息,尝试使用网页获取" % doubanid) + douban_info = self.douban.get_media_detail_from_web(doubanid) + if not douban_info: + log.warn("【Douban】%s 无权限访问,需要配置豆瓣Cookie" % doubanid) + # 随机休眠 + sleep(round(random.uniform(1, 5), 1)) + continue + media_type = MediaType.TV if douban_info.get("episodes_count") else MediaType.MOVIE + log.info("【Douban】%s:%s %s".strip() % (media_type.value, douban_info.get("title"), douban_info.get("year"))) + meta_info = MetaInfo(title="%s %s" % (douban_info.get("title"), douban_info.get("year") or "")) + meta_info.douban_id = doubanid + meta_info.type = media_type + meta_info.overview = douban_info.get("intro") + meta_info.poster_path = douban_info.get("cover_url") + rating = douban_info.get("rating", {}) or {} + meta_info.vote_average = rating.get("value") or "" + meta_info.imdb_id = douban_info.get("imdbid") + meta_info.user_name = info.get("user_name") + if meta_info not in media_list: + media_list.append(meta_info) + # 随机休眠 + sleep(round(random.uniform(1, 5), 1)) + return media_list diff --git a/app/downloader/__init__.py b/app/downloader/__init__.py new file mode 100644 index 0000000..c34c847 --- /dev/null +++ b/app/downloader/__init__.py @@ -0,0 +1 @@ +from .downloader import Downloader diff --git a/app/downloader/client/__init__.py b/app/downloader/client/__init__.py new file mode 100644 index 0000000..695d76c --- /dev/null +++ b/app/downloader/client/__init__.py @@ -0,0 +1,2 @@ +from .qbittorrent import Qbittorrent +from .transmission import Transmission diff --git a/app/downloader/client/_base.py b/app/downloader/client/_base.py new file mode 100644 index 0000000..9084f9d --- /dev/null +++ b/app/downloader/client/_base.py @@ -0,0 +1,152 @@ +import os.path +from abc import ABCMeta, abstractmethod + +from config import Config + + +class _IDownloadClient(metaclass=ABCMeta): + + @abstractmethod + def match(self, ctype): + """ + 匹配实例 + """ + pass + + @abstractmethod + def connect(self): + """ + 连接 + """ + pass + + @abstractmethod + def get_status(self): + """ + 检查连通性 + """ + pass + + @abstractmethod + def get_torrents(self, ids, status, tag): + """ + 按条件读取种子信息 + :param ids: 种子ID,单个ID或者ID列表 + :param status: 种子状态过滤 + :param tag: 种子标签过滤 + :return: 种子信息列表 + """ + pass + + @abstractmethod + def get_downloading_torrents(self, tag): + """ + 读取下载中的种子信息 + """ + pass + + @abstractmethod + def get_completed_torrents(self, tag): + """ + 读取下载完成的种子信息 + """ + pass + + @abstractmethod + def set_torrents_status(self, ids, tags=None): + """ + 迁移完成后设置种子标签为 已整理 + :param ids: 种子ID列表 + :param tags: 种子标签列表 + """ + pass + + @abstractmethod + def get_transfer_task(self, tag): + """ + 获取需要转移的种子列表 + """ + pass + + @abstractmethod + def get_remove_torrents(self, config): + """ + 获取需要清理的种子清单 + :param config: 删种策略 + :return: 种子ID列表 + """ + pass + + @abstractmethod + def add_torrent(self, **kwargs): + """ + 添加下载任务 + """ + pass + + @abstractmethod + def start_torrents(self, ids): + """ + 下载控制:开始 + """ + pass + + @abstractmethod + def stop_torrents(self, ids): + """ + 下载控制:停止 + """ + pass + + @abstractmethod + def delete_torrents(self, delete_file, ids): + """ + 删除种子 + """ + pass + + @abstractmethod + def get_download_dirs(self): + """ + 获取下载目录清单 + """ + pass + + @staticmethod + def get_replace_path(path): + """ + 对目录路径进行转换 + """ + if not path: + return "" + downloaddir = Config().get_config('downloaddir') or [] + path = os.path.normpath(path) + for attr in downloaddir: + if not attr.get("save_path") or not attr.get("container_path"): + continue + save_path = os.path.normpath(attr.get("save_path")) + container_path = os.path.normpath(attr.get("container_path")) + if path.startswith(save_path): + return path.replace(save_path, container_path) + return path + + @abstractmethod + def change_torrent(self, **kwargs): + """ + 修改种子状态 + """ + pass + + @abstractmethod + def get_downloading_progress(self): + """ + 获取下载进度 + """ + pass + + @abstractmethod + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/_py115.py b/app/downloader/client/_py115.py new file mode 100644 index 0000000..f7920fc --- /dev/null +++ b/app/downloader/client/_py115.py @@ -0,0 +1,182 @@ +import re +import time +from urllib import parse + +import requests + +from app.utils import RequestUtils, ExceptionUtils + + +class Py115: + cookie = None + user_agent = None + req = None + uid = None + sign = None + err = None + + def __init__(self, cookie): + self.cookie = cookie + self.req = RequestUtils(cookies=self.cookie, session=requests.Session()) + + # 登录 + def login(self): + if not self.getuid(): + return False + if not self.getsign(): + return False + return True + + # 获取目录ID + def getdirid(self, tdir): + try: + url = "https://webapi.115.com/files/getid?path=" + parse.quote(tdir or '/') + p = self.req.get_res(url=url) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = "获取目录 [{}]ID 错误:{}".format(tdir, rootobject["error"]) + return False, '' + return True, rootobject.get("id") + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False, '' + + # 获取sign + def getsign(self): + try: + self.sign = '' + url = "https://115.com/?ct=offline&ac=space&_=" + str(round(time.time() * 1000)) + p = self.req.get_res(url=url) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = "获取 SIGN 错误:{}".format(rootobject.get("error_msg")) + return False + self.sign = rootobject.get("sign") + return True + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False + + # 获取UID + def getuid(self): + try: + self.uid = '' + url = "https://webapi.115.com/files?aid=1&cid=0&o=user_ptime&asc=0&offset=0&show_dir=1&limit=30&code=&scid=&snap=0&natsort=1&star=1&source=&format=json" + p = self.req.get_res(url=url) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = "获取 UID 错误:{}".format(rootobject.get("error_msg")) + return False + self.uid = rootobject.get("uid") + return True + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False + + # 获取任务列表 + def gettasklist(self, page=1): + try: + tasks = [] + url = "https://115.com/web/lixian/?ct=lixian&ac=task_lists" + while True: + postdata = "page={}&uid={}&sign={}&time={}".format(page, self.uid, self.sign, + str(round(time.time() * 1000))) + p = self.req.post_res(url=url, params=postdata.encode('utf-8')) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = "获取任务列表错误:{}".format(rootobject["error"]) + return False, tasks + if rootobject.get("count") == 0: + break + tasks += rootobject.get("tasks") or [] + if page >= rootobject.get("page_count"): + break + return True, tasks + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False, [] + + # 添加任务 + def addtask(self, tdir, content): + try: + ret, dirid = self.getdirid(tdir) + if not ret: + return False, '' + + # 转换为磁力 + if re.match("^https*://", content): + try: + p = self.req.get_res(url=content) + if p and p.headers.get("Location"): + content = p.headers.get("Location") + except Exception as result: + ExceptionUtils.exception_traceback(result) + content = str(result).replace("No connection adapters were found for '", "").replace("'", "") + + url = "https://115.com/web/lixian/?ct=lixian&ac=add_task_url" + postdata = "url={}&savepath=&wp_path_id={}&uid={}&sign={}&time={}".format(parse.quote(content), dirid, + self.uid, self.sign, + str(round(time.time() * 1000))) + p = self.req.post_res(url=url, params=postdata.encode('utf-8')) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = rootobject.get("error_msg") + return False, '' + return True, rootobject.get("info_hash") + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False, '' + + # 删除任务 + def deltask(self, thash): + try: + url = "https://115.com/web/lixian/?ct=lixian&ac=task_del" + postdata = "hash[0]={}&uid={}&sign={}&time={}".format(thash, self.uid, self.sign, + str(round(time.time() * 1000))) + p = self.req.post_res(url=url, params=postdata.encode('utf-8')) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = rootobject.get("error_msg") + return False + return True + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False + + # 根据ID获取文件夹路径 + def getiddir(self, tid): + try: + path = '/' + url = "https://aps.115.com/natsort/files.php?aid=1&cid={}&o=file_name&asc=1&offset=0&show_dir=1&limit=40&code=&scid=&snap=0&natsort=1&record_open_time=1&source=&format=json&fc_mix=0&type=&star=&is_share=&suffix=&custom_order=0".format( + tid) + p = self.req.get_res(url=url) + if p: + rootobject = p.json() + if not rootobject.get("state"): + self.err = "获取 ID[{}]路径 错误:{}".format(id, rootobject["error"]) + return False, path + patharray = rootobject["path"] + for pathobject in patharray: + if pathobject.get("cid") == 0: + continue + path += pathobject.get("name") + '/' + if path == "/": + self.err = "文件路径不存在" + return False, path + return True, path + except Exception as result: + ExceptionUtils.exception_traceback(result) + self.err = "异常错误:{}".format(result) + return False, '/' diff --git a/app/downloader/client/client115.py b/app/downloader/client/client115.py new file mode 100644 index 0000000..8e7d625 --- /dev/null +++ b/app/downloader/client/client115.py @@ -0,0 +1,141 @@ +import log +from app.utils import StringUtils +from app.utils.types import DownloaderType +from config import Config +from app.downloader.client._base import _IDownloadClient +from app.downloader.client._py115 import Py115 + + +class Client115(_IDownloadClient): + schema = "client115" + client_type = DownloaderType.Client115.value + _client_config = {} + + downclient = None + lasthash = None + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('client115') + self.init_config() + self.connect() + + def init_config(self): + if self._client_config: + self.downclient = Py115(self._client_config.get("cookie")) + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.client_type] else False + + def connect(self): + self.downclient.login() + + def get_status(self): + if not self.downclient: + return False + ret = self.downclient.login() + if not ret: + log.info(self.downclient.err) + return False + return True + + def get_torrents(self, ids=None, status=None, **kwargs): + tlist = [] + if not self.downclient: + return tlist + ret, tasks = self.downclient.gettasklist(page=1) + if not ret: + log.info(f"【{self.client_type}】获取任务列表错误:{self.downclient.err}") + return tlist + if tasks: + for task in tasks: + if ids: + if task.get("info_hash") not in ids: + continue + if status: + if task.get("status") not in status: + continue + ret, tdir = self.downclient.getiddir(task.get("file_id")) + task["path"] = tdir + tlist.append(task) + + return tlist or [] + + def get_completed_torrents(self, **kwargs): + return self.get_torrents(status=[2]) + + def get_downloading_torrents(self, **kwargs): + return self.get_torrents(status=[0, 1]) + + def remove_torrents_tag(self, **kwargs): + pass + + def get_transfer_task(self, **kwargs): + pass + + def get_remove_torrents(self, **kwargs): + return [] + + def add_torrent(self, content, download_dir=None, **kwargs): + if not self.downclient: + return False + if isinstance(content, str): + ret, self.lasthash = self.downclient.addtask(tdir=download_dir, content=content) + if not ret: + log.error(f"【{self.client_type}】添加下载任务失败:{self.downclient.err}") + return None + return self.lasthash + else: + log.info(f"【{self.client_type}】暂时不支持非链接下载") + return None + + def delete_torrents(self, delete_file, ids): + if not self.downclient: + return False + return self.downclient.deltask(thash=ids) + + def start_torrents(self, ids): + pass + + def stop_torrents(self, ids): + pass + + def set_torrents_status(self, ids, **kwargs): + return self.delete_torrents(ids=ids, delete_file=False) + + def get_download_dirs(self): + return [] + + def change_torrent(self, **kwargs): + pass + + def get_downloading_progress(self, **kwargs): + """ + 获取正在下载的种子进度 + """ + Torrents = self.get_downloading_torrents() + DispTorrents = [] + for torrent in Torrents: + # 进度 + progress = round(torrent.get('percentDone'), 1) + state = "Downloading" + _dlspeed = StringUtils.str_filesize(torrent.get('peers')) + _upspeed = StringUtils.str_filesize(torrent.get('rateDownload')) + speed = "%s%sB/s %s%sB/s" % (chr(8595), _dlspeed, chr(8593), _upspeed) + DispTorrents.append({ + 'id': torrent.get('info_hash'), + 'name': torrent.get('name'), + 'speed': speed, + 'state': state, + 'progress': progress + }) + return DispTorrents + + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/pikpak.py b/app/downloader/client/pikpak.py new file mode 100644 index 0000000..501fe31 --- /dev/null +++ b/app/downloader/client/pikpak.py @@ -0,0 +1,153 @@ +import asyncio + +from pikpakapi import PikPakApi, DownloadStatus + +import log +from app.downloader.client._base import _IDownloadClient +from app.utils.types import DownloaderType +from config import Config + + +class PikPak(_IDownloadClient): + schema = "pikpak" + client_type = DownloaderType.PikPak.value + _client_config = {} + + downclient = None + lasthash = None + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('pikpak') + self.init_config() + self.connect() + + def init_config(self): + if self._client_config: + self.downclient = PikPakApi( + username=self._client_config.get("username"), + password=self._client_config.get("password"), + proxy=self._client_config.get("proxy"), + ) + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.client_type] else False + + def connect(self): + try: + asyncio.run(self.downclient.login()) + except Exception as err: + print(str(err)) + return + + def get_status(self): + if not self.downclient: + return False + try: + asyncio.run(self.downclient.login()) + if self.downclient.user_id is None: + log.info("PikPak 登录失败") + return False + except Exception as err: + log.error("PikPak 登录出错:%s" % str(err)) + return False + + return True + + def get_torrents(self, ids=None, status=None, **kwargs): + rv = [] + if self.downclient.user_id is None: + if self.get_status(): + return [], False + + if ids is not None: + for id in ids: + status = asyncio.run(self.downclient.get_task_status(id, '')) + if status == DownloadStatus.downloading: + rv.append({"id": id, "finish": False}) + if status == DownloadStatus.done: + rv.append({"id": id, "finish": True}) + return rv, True + + def get_completed_torrents(self, **kwargs): + return [] + + def get_downloading_torrents(self, **kwargs): + if self.downclient.user_id is None: + if self.get_status(): + return [] + try: + offline_list = asyncio.run(self.downclient.offline_list()) + return offline_list['tasks'] + except Exception as err: + print(str(err)) + return [] + + def get_transfer_task(self, **kwargs): + pass + + def get_remove_torrents(self, **kwargs): + return [] + + def add_torrent(self, content, download_dir=None, **kwargs): + try: + folder = asyncio.run( + self.downclient.path_to_id(download_dir, True)) + count = len(folder) + if count == 0: + print("create parent folder failed") + return None + else: + task = asyncio.run(self.downclient.offline_download( + content, folder[count - 1]["id"] + )) + return task["task"]["id"] + except Exception as e: + log.error("PikPak 添加离线下载任务失败: %s" % str(e)) + return None + + # 需要完成 + def delete_torrents(self, delete_file, ids): + pass + + def start_torrents(self, ids): + pass + + def stop_torrents(self, ids): + pass + + # 需要完成 + def set_torrents_status(self, ids, **kwargs): + pass + + def get_download_dirs(self): + return [] + + def change_torrent(self, **kwargs): + pass + + # 需要完成 + def get_downloading_progress(self, **kwargs): + """ + 获取正在下载的种子进度 + """ + Torrents = self.get_downloading_torrents() + DispTorrents = [] + for torrent in Torrents: + DispTorrents.append({ + 'id': torrent.get('id'), + 'file_id': torrent.get('file_id'), + 'name': torrent.get('file_name'), + 'nomenu': True, + 'noprogress': True + }) + return DispTorrents + + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/qbittorrent.py b/app/downloader/client/qbittorrent.py new file mode 100644 index 0000000..da9adc0 --- /dev/null +++ b/app/downloader/client/qbittorrent.py @@ -0,0 +1,530 @@ +import os +import re +import time +from datetime import datetime +from urllib import parse + +from pkg_resources import parse_version as v + +import log +import qbittorrentapi +from app.downloader.client._base import _IDownloadClient +from app.utils import ExceptionUtils, StringUtils +from app.utils.types import DownloaderType +from config import Config + + +class Qbittorrent(_IDownloadClient): + schema = "qbittorrent" + client_type = DownloaderType.QB.value + _client_config = {} + + _force_upload = False + _auto_management = False + qbc = None + ver = None + host = None + port = None + username = None + password = None + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('qbittorrent') + self.init_config() + self.connect() + + def init_config(self): + if self._client_config: + self.host = self._client_config.get('qbhost') + self.port = int(self._client_config.get('qbport')) if str(self._client_config.get('qbport')).isdigit() else 0 + self.username = self._client_config.get('qbusername') + self.password = self._client_config.get('qbpassword') + # 强制做种开关 + self._force_upload = self._client_config.get('force_upload') + # 自动管理模式开关 + self._auto_management = self._client_config.get('auto_management') + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.client_type] else False + + def connect(self): + if self.host and self.port: + self.qbc = self.__login_qbittorrent() + + def __login_qbittorrent(self): + """ + 连接qbittorrent + :return: qbittorrent对象 + """ + try: + # 登录 + qbt = qbittorrentapi.Client(host=self.host, + port=self.port, + username=self.username, + password=self.password, + VERIFY_WEBUI_CERTIFICATE=False, + REQUESTS_ARGS={'timeout': (10, 30)}) + try: + qbt.auth_log_in() + self.ver = qbt.app_version() + except qbittorrentapi.LoginFailed as e: + print(str(e)) + return qbt + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error(f"【{self.client_type}】qBittorrent连接出错:{str(err)}") + return None + + def get_status(self): + if not self.qbc: + return False + try: + return True if self.qbc.transfer_info() else False + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def get_torrents(self, ids=None, status=None, tag=None): + """ + 获取种子列表 + return: 种子列表, 是否发生异常 + """ + if not self.qbc: + return [], True + try: + torrents = self.qbc.torrents_info(torrent_hashes=ids, status_filter=status, tag=tag) + if self.is_ver_less_4_4(): + torrents = self.filter_torrent_by_tag(torrents, tag=tag) + return torrents or [], False + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [], True + + def get_completed_torrents(self, tag=None): + """ + 获取已完成的种子 + return: 种子列表, 是否发生异常 + """ + if not self.qbc: + return [] + torrents, _ = self.get_torrents(status=["completed"], tag=tag) + return torrents + + def get_downloading_torrents(self, tag=None): + """ + 获取正在下载的种子 + return: 种子列表, 是否发生异常 + """ + if not self.qbc: + return [] + torrents, _ = self.get_torrents(status=["downloading"], tag=tag) + return torrents + + def remove_torrents_tag(self, ids, tag): + """ + 移除种子Tag + :param ids: 种子Hash列表 + :param tag: 标签内容 + """ + try: + return self.qbc.torrents_delete_tags(torrent_hashes=ids, tags=tag) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def set_torrents_status(self, ids, tags=None): + if not self.qbc: + return + try: + # 打标签 + self.qbc.torrents_add_tags(tags="已整理", torrent_hashes=ids) + # 超级做种 + if self._force_upload: + self.qbc.torrents_set_force_start(enable=True, torrent_hashes=ids) + log.info(f"【{self.client_type}】设置qBittorrent种子状态成功") + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def torrents_set_force_start(self, ids): + """ + 设置强制作种 + """ + try: + self.qbc.torrents_set_force_start(enable=True, torrent_hashes=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def get_transfer_task(self, tag): + # 处理下载完成的任务 + torrents = self.get_completed_torrents(tag=tag) + trans_tasks = [] + for torrent in torrents: + # 判断标签是否包含"已整理" + if torrent.get("tags") and "已整理" in torrent.get("tags"): + continue + path = torrent.get("save_path") + if not path: + continue + content_path = torrent.get("content_path") + if content_path: + trans_name = content_path.replace(path, "") + if trans_name.startswith('/') or trans_name.startswith('\\'): + trans_name = trans_name[1:] + else: + trans_name = torrent.get('name') + true_path = self.get_replace_path(path) + trans_tasks.append( + {'path': os.path.join(true_path, trans_name).replace("\\", "/"), 'id': torrent.get('hash')}) + return trans_tasks + + def get_remove_torrents(self, config=None): + if not config: + return [] + remove_torrents = [] + remove_torrents_ids = [] + torrents, error_flag = self.get_torrents(tag=config.get("filter_tags")) + if error_flag: + return [] + ratio = config.get("ratio") + # 做种时间 单位:小时 + seeding_time = config.get("seeding_time") + # 大小 单位:GB + size = config.get("size") + minsize = size[0] * 1024 * 1024 * 1024 if size else 0 + maxsize = size[-1] * 1024 * 1024 * 1024 if size else 0 + # 平均上传速度 单位 KB/s + upload_avs = config.get("upload_avs") + savepath_key = config.get("savepath_key") + tracker_key = config.get("tracker_key") + qb_state = config.get("qb_state") + qb_category = config.get("qb_category") + for torrent in torrents: + date_done = torrent.completion_on if torrent.completion_on > 0 else torrent.added_on + date_now = int(time.mktime(datetime.now().timetuple())) + torrent_seeding_time = date_now - date_done if date_done else 0 + torrent_upload_avs = torrent.uploaded / torrent_seeding_time if torrent_seeding_time else 0 + if ratio and torrent.ratio <= ratio: + continue + if seeding_time and torrent_seeding_time <= seeding_time * 3600: + continue + if size and (torrent.size >= maxsize or torrent.size <= minsize): + continue + if upload_avs and torrent_upload_avs >= upload_avs * 1024: + continue + if savepath_key and not re.findall(savepath_key, torrent.save_path, re.I): + continue + if tracker_key and not re.findall(tracker_key, torrent.tracker, re.I): + continue + if qb_state and torrent.state not in qb_state: + continue + if qb_category and torrent.category not in qb_category: + continue + remove_torrents.append({ + "id": torrent.hash, + "name": torrent.name, + "site": parse.urlparse(torrent.tracker).netloc.split(".")[-2] if torrent.tracker else "", + "size": torrent.size + }) + remove_torrents_ids.append(torrent.hash) + if config.get("samedata") and remove_torrents: + remove_torrents_plus = [] + for remove_torrent in remove_torrents: + name = remove_torrent.get("name") + size = remove_torrent.get("size") + for torrent in torrents: + if torrent.name == name and torrent.size == size and torrent.hash not in remove_torrents_ids: + remove_torrents_plus.append({ + "id": torrent.hash, + "name": torrent.name, + "site": parse.urlparse(torrent.tracker).netloc.split(".")[-2], + "size": torrent.size + }) + remove_torrents_plus += remove_torrents + return remove_torrents_plus + return remove_torrents + + def __get_last_add_torrentid_by_tag(self, tag, status=None): + """ + 根据种子的下载链接获取下载中或暂停的钟子的ID + :return: 种子ID + """ + try: + torrents, _ = self.get_torrents(status=status, tag=tag) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return None + if torrents: + return torrents[0].get("hash") + else: + return None + + def get_torrent_id_by_tag(self, tag, status=None): + """ + 通过标签多次尝试获取刚添加的种子ID,并移除标签 + """ + torrent_id = None + # QB添加下载后需要时间,重试5次每次等待5秒 + for i in range(1, 6): + time.sleep(5) + torrent_id = self.__get_last_add_torrentid_by_tag(tag=tag, + status=status) + if torrent_id is None: + continue + else: + self.remove_torrents_tag(torrent_id, tag) + break + return torrent_id + + def add_torrent(self, + content, + is_paused=False, + download_dir=None, + tag=None, + category=None, + content_layout=None, + upload_limit=None, + download_limit=None, + ratio_limit=None, + seeding_time_limit=None, + cookie=None + ): + """ + 添加种子 + :param content: 种子urls或文件 + :param is_paused: 添加后暂停 + :param tag: 标签 + :param download_dir: 下载路径 + :param category: 分类 + :param content_layout: 布局 + :param upload_limit: 上传限速 Kb/s + :param download_limit: 下载限速 Kb/s + :param ratio_limit: 分享率限制 + :param seeding_time_limit: 做种时间限制 + :param cookie: 站点Cookie用于辅助下载种子 + :return: bool + """ + if not self.qbc or not content: + return False + if isinstance(content, str): + urls = content + torrent_files = None + else: + urls = None + torrent_files = content + if download_dir: + save_path = download_dir + else: + save_path = None + if not category: + category = None + if tag: + tags = tag + else: + tags = None + if not content_layout: + content_layout = None + if upload_limit: + upload_limit = int(upload_limit) * 1024 + else: + upload_limit = None + if download_limit: + download_limit = int(download_limit) * 1024 + else: + download_limit = None + if ratio_limit: + ratio_limit = round(float(ratio_limit), 2) + else: + ratio_limit = None + if seeding_time_limit: + seeding_time_limit = int(seeding_time_limit) + else: + seeding_time_limit = None + try: + if self._auto_management: + use_auto_torrent_management = True + else: + use_auto_torrent_management = False + qbc_ret = self.qbc.torrents_add(urls=urls, + torrent_files=torrent_files, + save_path=save_path, + category=category, + is_paused=is_paused, + tags=tags, + content_layout=content_layout, + upload_limit=upload_limit, + download_limit=download_limit, + ratio_limit=ratio_limit, + seeding_time_limit=seeding_time_limit, + use_auto_torrent_management=use_auto_torrent_management, + cookie=cookie) + return True if qbc_ret and str(qbc_ret).find("Ok") != -1 else False + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def start_torrents(self, ids): + if not self.qbc: + return False + try: + return self.qbc.torrents_resume(torrent_hashes=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def stop_torrents(self, ids): + if not self.qbc: + return False + try: + return self.qbc.torrents_pause(torrent_hashes=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def delete_torrents(self, delete_file, ids): + if not self.qbc: + return False + if not ids: + return False + try: + ret = self.qbc.torrents_delete(delete_files=delete_file, torrent_hashes=ids) + return ret + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def get_files(self, tid): + try: + return self.qbc.torrents_files(torrent_hash=tid) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return None + + def set_files(self, **kwargs): + """ + 设置下载文件的状态,priority为0为不下载,priority为1为下载 + """ + if not kwargs.get("torrent_hash") or not kwargs.get("file_ids"): + return False + try: + self.qbc.torrents_file_priority(torrent_hash=kwargs.get("torrent_hash"), + file_ids=kwargs.get("file_ids"), + priority=kwargs.get("priority")) + return True + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def set_torrent_tag(self, **kwargs): + pass + + def get_download_dirs(self): + if not self.qbc: + return [] + ret_dirs = [] + try: + categories = self.qbc.torrents_categories(requests_args={'timeout': (5, 10)}) or {} + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [] + for category in categories.values(): + if category and category.get("savePath") and category.get("savePath") not in ret_dirs: + ret_dirs.append(category.get("savePath")) + return ret_dirs + + def set_uploadspeed_limit(self, ids, limit): + """ + 设置上传限速,单位bytes/sec + """ + if not self.qbc: + return + if not ids or not limit: + return + self.qbc.torrents_set_upload_limit(limit=int(limit), + torrent_hashes=ids) + + def set_downloadspeed_limit(self, ids, limit): + """ + 设置下载限速,单位bytes/sec + """ + if not self.qbc: + return + if not ids or not limit: + return + self.qbc.torrents_set_download_limit(limit=int(limit), + torrent_hashes=ids) + + def is_ver_less_4_4(self): + return v(self.ver) < v("v4.4.0") + + @staticmethod + def filter_torrent_by_tag(torrents, tag): + if not tag: + return torrents + if not isinstance(tag, list): + tag = [tag] + results = [] + for torrent in torrents: + include_flag = True + for t in tag: + if t and t not in torrent.get("tags"): + include_flag = False + break + if include_flag: + results.append(torrent) + return results + + def change_torrent(self, **kwargs): + """ + 修改种子状态 + """ + pass + + def get_downloading_progress(self, tag=None): + """ + 获取正在下载的种子进度 + """ + Torrents = self.get_downloading_torrents(tag=tag) + DispTorrents = [] + for torrent in Torrents: + # 进度 + progress = round(torrent.get('progress') * 100, 1) + if torrent.get('state') in ['pausedDL']: + state = "Stoped" + speed = "已暂停" + else: + state = "Downloading" + _dlspeed = StringUtils.str_filesize(torrent.get('dlspeed')) + _upspeed = StringUtils.str_filesize(torrent.get('upspeed')) + if progress >= 100: + speed = "%s%sB/s %s%sB/s" % (chr(8595), _dlspeed, chr(8593), _upspeed) + else: + eta = StringUtils.str_timelong(torrent.get('eta')) + speed = "%s%sB/s %s%sB/s %s" % (chr(8595), _dlspeed, chr(8593), _upspeed, eta) + # 主键 + DispTorrents.append({ + 'id': torrent.get('hash'), + 'name': torrent.get('name'), + 'speed': speed, + 'state': state, + 'progress': progress + }) + return DispTorrents + + def set_speed_limit(self, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not self.qbc: + return + try: + if self.qbc.transfer.upload_limit != upload_limit: + self.qbc.transfer.upload_limit = upload_limit + if self.qbc.transfer.download_limit != download_limit: + self.qbc.transfer.download_limit = download_limit + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False diff --git a/app/downloader/client/transmission.py b/app/downloader/client/transmission.py new file mode 100644 index 0000000..d705047 --- /dev/null +++ b/app/downloader/client/transmission.py @@ -0,0 +1,526 @@ +import os.path +import re +import time +from datetime import datetime + +import transmission_rpc + +import log +from app.utils import ExceptionUtils, StringUtils +from app.utils.types import DownloaderType +from config import Config +from app.downloader.client._base import _IDownloadClient + + +class Transmission(_IDownloadClient): + schema = "transmission" + client_type = DownloaderType.TR.value + _client_config = {} + + # 参考transmission web,仅查询需要的参数,加速种子检索 + _trarg = ["id", "name", "status", "labels", "hashString", "totalSize", "percentDone", "addedDate", "trackerStats", + "leftUntilDone", "rateDownload", "rateUpload", "recheckProgress", "rateDownload", "rateUpload", + "peersGettingFromUs", "peersSendingToUs", "uploadRatio", "uploadedEver", "downloadedEver", "downloadDir", + "error", "errorString", "doneDate", "queuePosition", "activityDate", "trackers"] + trc = None + host = None + port = None + username = None + password = None + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('transmission') + self.init_config() + self.connect() + + def init_config(self): + if self._client_config: + self.host = self._client_config.get('trhost') + self.port = int(self._client_config.get('trport')) if str(self._client_config.get('trport')).isdigit() else 0 + self.username = self._client_config.get('trusername') + self.password = self._client_config.get('trpassword') + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.client_type] else False + + def connect(self): + if self.host and self.port: + self.trc = self.__login_transmission() + + def __login_transmission(self): + """ + 连接transmission + :return: transmission对象 + """ + try: + # 登录 + trt = transmission_rpc.Client(host=self.host, + port=self.port, + username=self.username, + password=self.password, + timeout=30) + return trt + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error(f"【{self.client_type}】transmission连接出错:{str(err)}") + return None + + def get_status(self): + return True if self.trc else False + + def get_torrents(self, ids=None, status=None, tag=None): + """ + 获取种子列表 + 返回结果 种子列表, 是否有错误 + """ + if not self.trc: + return [], True + if isinstance(ids, list): + ids = [int(x) for x in ids if str(x).isdigit()] + elif str(ids).isdigit(): + ids = int(ids) + try: + torrents = self.trc.get_torrents(ids=ids, arguments=self._trarg) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [], True + if status and not isinstance(status, list): + status = [status] + if tag and not isinstance(tag, list): + tag = [tag] + ret_torrents = [] + for torrent in torrents: + if status and torrent.status not in status: + continue + labels = torrent.labels if hasattr(torrent, "labels") else [] + include_flag = True + if tag: + for t in tag: + if t and t not in labels: + include_flag = False + break + if include_flag: + ret_torrents.append(torrent) + return ret_torrents, False + + def get_completed_torrents(self, tag=None): + """ + 获取已完成的种子列表 + return 种子列表, 是否有错误 + """ + if not self.trc: + return [] + try: + torrents, _ = self.get_torrents(status=["seeding", "seed_pending"], tag=tag) + return torrents + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [] + + def get_downloading_torrents(self, tag=None): + """ + 获取正在下载的种子列表 + return 种子列表, 是否有错误 + """ + if not self.trc: + return [] + try: + torrents, _ = self.get_torrents(status=["downloading", "download_pending"], tag=tag) + return torrents + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [] + + def set_torrents_status(self, ids, tags=None): + if not self.trc: + return + if isinstance(ids, list): + ids = [int(x) for x in ids if str(x).isdigit()] + elif str(ids).isdigit(): + ids = int(ids) + # 合成标签 + if tags: + if not isinstance(tags, list): + tags = [tags, "已整理"] + else: + tags.append("已整理") + else: + tags = ["已整理"] + # 打标签 + try: + self.trc.change_torrent(labels=tags, ids=ids) + log.info(f"【{self.client_type}】设置transmission种子标签成功") + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def set_torrent_tag(self, tid, tag): + if not tid or not tag: + return + try: + self.trc.change_torrent(labels=tag, ids=int(tid)) + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def change_torrent(self, + tid, + tag=None, + upload_limit=None, + download_limit=None, + ratio_limit=None, + seeding_time_limit=None): + """ + 设置种子 + :param tid: ID + :param tag: 标签 + :param upload_limit: 上传限速 Kb/s + :param download_limit: 下载限速 Kb/s + :param ratio_limit: 分享率限制 + :param seeding_time_limit: 做种时间限制 + :return: bool + """ + if not tid: + return + else: + ids = int(tid) + if tag: + if isinstance(tag, list): + labels = tag + else: + labels = [tag] + else: + labels = [] + if upload_limit: + uploadLimited = True + uploadLimit = int(upload_limit) + else: + uploadLimited = False + uploadLimit = 0 + if download_limit: + downloadLimited = True + downloadLimit = int(download_limit) + else: + downloadLimited = False + downloadLimit = 0 + if ratio_limit: + seedRatioMode = 1 + seedRatioLimit = round(float(ratio_limit), 2) + else: + seedRatioMode = 2 + seedRatioLimit = 0 + if seeding_time_limit: + seedIdleMode = 1 + seedIdleLimit = int(seeding_time_limit) + else: + seedIdleMode = 2 + seedIdleLimit = 0 + try: + self.trc.change_torrent(ids=ids, + labels=labels, + uploadLimited=uploadLimited, + uploadLimit=uploadLimit, + downloadLimited=downloadLimited, + downloadLimit=downloadLimit, + seedRatioMode=seedRatioMode, + seedRatioLimit=seedRatioLimit, + seedIdleMode=seedIdleMode, + seedIdleLimit=seedIdleLimit) + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def get_transfer_task(self, tag): + # 处理所有任务 + torrents = self.get_completed_torrents(tag=tag) + trans_tasks = [] + for torrent in torrents: + # 3.0版本以下的Transmission没有labels + if not hasattr(torrent, "labels"): + log.error(f"【{self.client_type}】当前transmission版本可能过低,无labels属性,请安装3.0以上版本!") + break + if torrent.labels and "已整理" in torrent.labels: + continue + path = torrent.download_dir + if not path: + continue + true_path = self.get_replace_path(path) + trans_tasks.append({ + 'path': os.path.join(true_path, torrent.name).replace("\\", "/"), + 'id': torrent.id, + 'tags': torrent.labels + }) + return trans_tasks + + def get_remove_torrents(self, config=None): + if not config: + return [] + remove_torrents = [] + remove_torrents_ids = [] + torrents, error_flag = self.get_torrents() + if error_flag: + return [] + tags = config.get("filter_tags") + ratio = config.get("ratio") + # 做种时间 单位:小时 + seeding_time = config.get("seeding_time") + # 大小 单位:GB + size = config.get("size") + minsize = size[0]*1024*1024*1024 if size else 0 + maxsize = size[-1]*1024*1024*1024 if size else 0 + # 平均上传速度 单位 KB/s + upload_avs = config.get("upload_avs") + savepath_key = config.get("savepath_key") + tracker_key = config.get("tracker_key") + tr_state = config.get("tr_state") + tr_error_key = config.get("tr_error_key") + for torrent in torrents: + date_done = torrent.date_done or torrent.date_added + date_now = int(time.mktime(datetime.now().timetuple())) + torrent_seeding_time = date_now - int(time.mktime(date_done.timetuple())) if date_done else 0 + torrent_uploaded = torrent.ratio * torrent.total_size + torrent_upload_avs = torrent_uploaded / torrent_seeding_time if torrent_seeding_time else 0 + if ratio and torrent.ratio <= ratio: + continue + if seeding_time and torrent_seeding_time <= seeding_time*3600: + continue + if size and (torrent.total_size >= maxsize or torrent.total_size <= minsize): + continue + if upload_avs and torrent_upload_avs >= upload_avs*1024: + continue + if savepath_key and not re.findall(savepath_key, torrent.download_dir, re.I): + continue + if tracker_key: + if not torrent.trackers: + continue + else: + tacker_key_flag = False + for tracker in torrent.trackers: + if re.findall(tracker_key, tracker.get("announce", ""), re.I): + tacker_key_flag = True + break + if not tacker_key_flag: + continue + if tr_state and torrent.status not in tr_state: + continue + if tr_error_key and not re.findall(tr_error_key, torrent.error_string, re.I): + continue + labels = set(torrent.labels) + if tags and (not labels or not set(tags).issubset(labels)): + continue + remove_torrents.append({ + "id": torrent.id, + "name": torrent.name, + "site": torrent.trackers[0].get("sitename"), + "size": torrent.total_size + }) + remove_torrents_ids.append(torrent.id) + if config.get("samedata") and remove_torrents: + remove_torrents_plus = [] + for remove_torrent in remove_torrents: + name = remove_torrent.get("name") + size = remove_torrent.get("size") + for torrent in torrents: + if torrent.name == name and torrent.total_size == size and torrent.id not in remove_torrents_ids: + remove_torrents_plus.append({ + "id": torrent.id, + "name": torrent.name, + "site": torrent.trackers[0].get("sitename") if torrent.trackers else "", + "size": torrent.total_size + }) + remove_torrents_plus += remove_torrents + return remove_torrents_plus + return remove_torrents + + def add_torrent(self, content, + is_paused=False, + download_dir=None, + upload_limit=None, + download_limit=None, + cookie=None, + **kwargs): + try: + ret = self.trc.add_torrent(torrent=content, + download_dir=download_dir, + paused=is_paused, + cookies=cookie) + if ret and ret.id: + if upload_limit: + self.set_uploadspeed_limit(ret.id, int(upload_limit)) + if download_limit: + self.set_downloadspeed_limit(ret.id, int(download_limit)) + return ret + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def start_torrents(self, ids): + if not self.trc: + return False + if isinstance(ids, list): + ids = [int(x) for x in ids if str(x).isdigit()] + elif str(ids).isdigit(): + ids = int(ids) + try: + return self.trc.start_torrent(ids=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def stop_torrents(self, ids): + if not self.trc: + return False + if isinstance(ids, list): + ids = [int(x) for x in ids if str(x).isdigit()] + elif str(ids).isdigit(): + ids = int(ids) + try: + return self.trc.stop_torrent(ids=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def delete_torrents(self, delete_file, ids): + if not self.trc: + return False + if not ids: + return False + if isinstance(ids, list): + ids = [int(x) for x in ids if str(x).isdigit()] + elif str(ids).isdigit(): + ids = int(ids) + try: + return self.trc.remove_torrent(delete_data=delete_file, ids=ids) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def get_files(self, tid): + """ + 获取种子文件列表 + """ + if not tid: + return None + try: + torrent = self.trc.get_torrent(tid) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return None + if torrent: + return torrent.files() + else: + return None + + def set_files(self, **kwargs): + """ + 设置下载文件的状态 + { + : { + : { + 'priority': , + 'selected': + }, + ... + }, + ... + } + """ + if not kwargs.get("file_info"): + return False + try: + self.trc.set_files(kwargs.get("file_info")) + return True + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False + + def get_download_dirs(self): + if not self.trc: + return [] + try: + return [self.trc.get_session(timeout=10).download_dir] + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [] + + def set_uploadspeed_limit(self, ids, limit): + """ + 设置上传限速,单位 KB/sec + """ + if not self.trc: + return + if not ids or not limit: + return + if not isinstance(ids, list): + ids = int(ids) + else: + ids = [int(x) for x in ids if str(x).isdigit()] + self.trc.change_torrent(ids, uploadLimit=int(limit)) + + def set_downloadspeed_limit(self, ids, limit): + """ + 设置下载限速,单位 KB/sec + """ + if not self.trc: + return + if not ids or not limit: + return + if not isinstance(ids, list): + ids = int(ids) + else: + ids = [int(x) for x in ids if str(x).isdigit()] + self.trc.change_torrent(ids, downloadLimit=int(limit)) + + def get_downloading_progress(self, tag=None): + """ + 获取正在下载的种子进度 + """ + Torrents = self.get_downloading_torrents(tag=tag) + DispTorrents = [] + for torrent in Torrents: + if torrent.status in ['stopped']: + state = "Stoped" + speed = "已暂停" + else: + state = "Downloading" + _dlspeed = StringUtils.str_filesize(torrent.rateDownload) + _upspeed = StringUtils.str_filesize(torrent.rateUpload) + speed = "%s%sB/s %s%sB/s" % (chr(8595), _dlspeed, chr(8593), _upspeed) + # 进度 + progress = round(torrent.progress) + DispTorrents.append({ + 'id': torrent.id, + 'name': torrent.name, + 'speed': speed, + 'state': state, + 'progress': progress + }) + return DispTorrents + + def set_speed_limit(self, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not self.trc: + return + try: + session = self.trc.get_session() + download_limit_enabled = True if download_limit else False + upload_limit_enabled = True if upload_limit else False + if download_limit_enabled == session.speed_limit_down_enabled and \ + upload_limit_enabled == session.speed_limit_up_enabled and \ + download_limit == session.speed_limit_down and \ + upload_limit == session.speed_limit_up: + return + self.trc.set_session( + speed_limit_down=download_limit if download_limit != session.speed_limit_down + else session.speed_limit_down, + speed_limit_up=upload_limit if upload_limit != session.speed_limit_up + else session.speed_limit_up, + speed_limit_down_enabled=download_limit_enabled, + speed_limit_up_enabled=upload_limit_enabled + ) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False diff --git a/app/downloader/downloader.py b/app/downloader/downloader.py new file mode 100644 index 0000000..1343564 --- /dev/null +++ b/app/downloader/downloader.py @@ -0,0 +1,1081 @@ +import os +from threading import Lock + +import log +from app.conf import ModuleConf +from app.filetransfer import FileTransfer +from app.helper import DbHelper, ThreadHelper, SubmoduleHelper +from app.media import Media +from app.media.meta import MetaInfo +from app.mediaserver import MediaServer +from app.message import Message +from app.sites import Sites +from app.subtitle import Subtitle +from app.conf import SystemConfig +from app.utils import Torrent, StringUtils, SystemUtils, ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import MediaType, DownloaderType, SearchType, RmtMode +from config import Config, PT_TAG, RMT_MEDIAEXT + +lock = Lock() +client_lock = Lock() + + +@singleton +class Downloader: + clients = {} + _downloader_schema = [] + _default_client_type = None + _pt_monitor_only = None + _download_order = None + _pt_rmt_mode = None + _downloaddir = [] + _download_setting = {} + + message = None + mediaserver = None + filetransfer = None + media = None + sites = None + dbhelper = None + systemconfig = None + + def __init__(self): + self._downloader_schema = SubmoduleHelper.import_submodules( + 'app.downloader.client', + filter_func=lambda _, obj: hasattr(obj, 'schema') + ) + log.debug(f"【Downloader】加载下载器:{self._downloader_schema}") + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.message = Message() + self.mediaserver = MediaServer() + self.filetransfer = FileTransfer() + self.media = Media() + self.sites = Sites() + self.systemconfig = SystemConfig() + # 下载器配置 + pt = Config().get_config('pt') + if pt: + self._default_client_type = ModuleConf.DOWNLOADER_DICT.get(pt.get('pt_client')) or DownloaderType.QB + self._pt_monitor_only = pt.get("pt_monitor_only") + self._download_order = pt.get("download_order") + self._pt_rmt_mode = ModuleConf.RMT_MODES.get(pt.get("rmt_mode", "copy"), RmtMode.COPY) + # 下载目录配置 + self._downloaddir = Config().get_config('downloaddir') or [] + # 下载设置 + self._download_setting = { + "-1": { + "id": -1, + "name": "预设", + "category": '', + "tags": PT_TAG, + "content_layout": 0, + "is_paused": 0, + "upload_limit": 0, + "download_limit": 0, + "ratio_limit": 0, + "seeding_time_limit": 0, + "downloader": ""} + } + download_settings = self.dbhelper.get_download_setting() + for download_setting in download_settings: + self._download_setting[str(download_setting.ID)] = { + "id": download_setting.ID, + "name": download_setting.NAME, + "category": download_setting.CATEGORY, + "tags": download_setting.TAGS, + "content_layout": download_setting.CONTENT_LAYOUT, + "is_paused": download_setting.IS_PAUSED, + "upload_limit": download_setting.UPLOAD_LIMIT, + "download_limit": download_setting.DOWNLOAD_LIMIT, + "ratio_limit": download_setting.RATIO_LIMIT / 100, + "seeding_time_limit": download_setting.SEEDING_TIME_LIMIT, + "downloader": download_setting.DOWNLOADER} + + def __build_class(self, ctype, conf=None): + for downloader_schema in self._downloader_schema: + try: + if downloader_schema.match(ctype): + return downloader_schema(conf) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + @property + def default_client(self): + return self.__get_client(self._default_client_type) + + def __get_client(self, ctype: DownloaderType, conf: dict = None): + if not ctype: + return None + with client_lock: + if not self.clients.get(ctype.value): + self.clients[ctype.value] = self.__build_class(ctype.value, conf) + return self.clients.get(ctype.value) + + def download(self, + media_info, + is_paused=None, + tag=None, + download_dir=None, + download_setting=None, + torrent_file=None): + """ + 添加下载任务,根据当前使用的下载器分别调用不同的客户端处理 + :param media_info: 需下载的媒体信息,含URL地址 + :param is_paused: 是否暂停下载 + :param tag: 种子标签 + :param download_dir: 指定下载目录 + :param download_setting: 下载设置id + :param torrent_file: 种子文件路径 + :return: 种子或状态,错误信息 + """ + # 标题 + title = media_info.org_string + # 详情页面 + page_url = media_info.page_url + # 默认值 + _xpath, _hash, site_info, dl_files_folder, dl_files, retmsg = None, False, {}, "", [], "" + # 有种子文件时解析种子信息 + if torrent_file: + url = os.path.basename(torrent_file) + content, dl_files_folder, dl_files, retmsg = Torrent().read_torrent_content(torrent_file) + # 没有种子文件解析链接 + else: + url = media_info.enclosure + if not url: + return None, "下载链接为空" + # 获取种子内容,磁力链不解析 + if url.startswith("magnet:"): + content = url + else: + # [XPATH]为需从详情页面解析磁力链 + if url.startswith("["): + _xpath = url[1:-1] + url = page_url + # #XPATH#为需从详情页面解析磁力Hash + elif url.startswith("#"): + _xpath = url[1:-1] + _hash = True + url = page_url + # 从详情页面XPATH解析下载链接 + if _xpath: + content = self.sites.parse_site_download_url(page_url=url, + xpath=_xpath) + if not content: + return None, "无法从详情页面:%s 解析出下载链接" % url + # 解析出磁力链,补充Trackers + if content.startswith("magnet:"): + content = Torrent.add_trackers_to_magnet(url=content, title=title) + # 解析出来的是HASH值,转换为磁力链 + elif _hash: + content = Torrent.convert_hash_to_magnet(hash_text=content, title=title) + if not content: + return None, "%s 转换磁力链失败" % content + # 从HTTP链接下载种子 + else: + # 获取Cookie和ua等 + site_info = self.sites.get_site_attr(url) + # 下载种子文件,并读取信息 + _, content, dl_files_folder, dl_files, retmsg = Torrent().get_torrent_info( + url=url, + cookie=site_info.get("cookie"), + ua=site_info.get("ua"), + referer=page_url if site_info.get("referer") else None, + proxy=site_info.get("proxy") + ) + # 解析完成 + if retmsg: + log.warn("【Downloader】%s" % retmsg) + if not content: + return None, retmsg + + # 下载设置 + if not download_setting and media_info.site: + download_setting = self.sites.get_site_download_setting(media_info.site) + if download_setting: + download_attr = self.get_download_setting(download_setting) \ + or self.get_download_setting(self.get_default_download_setting()) + else: + download_attr = self.get_download_setting(self.get_default_download_setting()) + # 下载器类型 + dl_type = self.__get_client_type(download_attr.get("downloader")) or self._default_client_type + # 下载器客户端 + downloader = self.__get_client(dl_type) + + # 开始添加下载 + try: + # 分类 + category = download_attr.get("category") + # 合并TAG + tags = download_attr.get("tags") + if tags: + tags = tags.split(";") + if tag: + tags.append(tag) + else: + if tag: + tags = [tag] + # 布局 + content_layout = download_attr.get("content_layout") + if content_layout == 1: + content_layout = "Original" + elif content_layout == 2: + content_layout = "Subfolder" + elif content_layout == 3: + content_layout = "NoSubfolder" + else: + content_layout = "" + # 暂停 + if is_paused is None: + is_paused = StringUtils.to_bool(download_attr.get("is_paused")) + else: + is_paused = True if is_paused else False + # 上传限速 + upload_limit = download_attr.get("upload_limit") + # 下载限速 + download_limit = download_attr.get("download_limit") + # 分享率 + ratio_limit = download_attr.get("ratio_limit") + # 做种时间 + seeding_time_limit = download_attr.get("seeding_time_limit") + # 下载目录 + if not download_dir: + download_info = self.__get_download_dir_info(media_info) + download_dir = download_info.get('path') + download_label = download_info.get('label') + if not category: + category = download_label + # 添加下载 + print_url = content if isinstance(content, str) else url + if is_paused: + log.info("【Downloader】添加下载任务并暂停:%s,目录:%s,Url:%s" % (title, download_dir, print_url)) + else: + log.info("【Downloader】添加下载任务:%s,目录:%s,Url:%s" % (title, download_dir, print_url)) + if dl_type == DownloaderType.TR: + ret = downloader.add_torrent(content, + is_paused=is_paused, + download_dir=download_dir, + cookie=site_info.get("cookie")) + if ret: + downloader.change_torrent(tid=ret.id, + tag=tags, + upload_limit=upload_limit, + download_limit=download_limit, + ratio_limit=ratio_limit, + seeding_time_limit=seeding_time_limit) + elif dl_type == DownloaderType.QB: + ret = downloader.add_torrent(content, + is_paused=is_paused, + download_dir=download_dir, + tag=tags, + category=category, + content_layout=content_layout, + upload_limit=upload_limit, + download_limit=download_limit, + ratio_limit=ratio_limit, + seeding_time_limit=seeding_time_limit, + cookie=site_info.get("cookie")) + else: + ret = downloader.add_torrent(content, + is_paused=is_paused, + tag=tags, + download_dir=download_dir, + category=category) + # 添加下载成功 + if ret: + # 登记下载历史 + self.dbhelper.insert_download_history(media_info) + # 下载站点字幕文件 + if page_url \ + and download_dir \ + and dl_files \ + and site_info \ + and site_info.get("subtitle"): + # 下载访问目录 + visit_dir = self.get_download_visit_dir(download_dir) + if visit_dir: + if dl_files_folder: + subtitle_dir = os.path.join(visit_dir, dl_files_folder) + else: + subtitle_dir = visit_dir + ThreadHelper().start_thread( + Subtitle().download_subtitle_from_site, + (media_info, site_info.get("cookie"), site_info.get("ua"), subtitle_dir) + ) + return ret, "" + else: + return ret, "请检查下载任务是否已存在" + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【Downloader】添加下载任务出错:%s" % str(e)) + return None, str(e) + + def transfer(self): + """ + 转移下载完成的文件,进行文件识别重命名到媒体库目录 + """ + if self.default_client: + try: + lock.acquire() + if self._pt_monitor_only: + tag = [PT_TAG] + else: + tag = None + trans_tasks = self.default_client.get_transfer_task(tag=tag) + if trans_tasks: + log.info("【Downloader】开始转移下载文件...") + else: + return + for task in trans_tasks: + done_flag, done_msg = self.filetransfer.transfer_media(in_from=self._default_client_type, + in_path=task.get("path"), + rmt_mode=self._pt_rmt_mode) + if not done_flag: + log.warn("【Downloader】%s 转移失败:%s" % (task.get("path"), done_msg)) + self.default_client.set_torrents_status(ids=task.get("id"), + tags=task.get("tags")) + else: + if self._pt_rmt_mode in [RmtMode.MOVE, RmtMode.RCLONE, RmtMode.MINIO]: + log.warn("【Downloader】移动模式下删除种子文件:%s" % task.get("id")) + self.default_client.delete_torrents(delete_file=True, ids=task.get("id")) + else: + self.default_client.set_torrents_status(ids=task.get("id"), + tags=task.get("tags")) + log.info("【Downloader】下载文件转移结束") + finally: + lock.release() + + def get_remove_torrents(self, downloader=None, config=None): + """ + 查询符合删种策略的种子信息 + :return: 符合删种策略的种子信息列表 + """ + if not downloader or not config: + return [] + _client = self.__get_client(downloader) + if self._pt_monitor_only: + config["filter_tags"] = config["tags"] + [PT_TAG] + else: + config["filter_tags"] = config["tags"] + torrents = _client.get_remove_torrents(config=config) + torrents.sort(key=lambda x: x.get("name")) + return torrents + + def get_downloading_torrents(self): + """ + 查询正在下载中的种子信息 + :return: 客户端类型,下载中的种子信息列表 + """ + if not self.default_client: + return self._default_client_type, [] + if self._pt_monitor_only: + tag = [PT_TAG] + else: + tag = None + try: + return self._default_client_type, self.default_client.get_downloading_torrents(tag=tag) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return self._default_client_type, [] + + def get_downloading_progress(self): + """ + 查询正在下载中的进度信息 + """ + if not self.default_client: + return [] + if self._pt_monitor_only: + tag = [PT_TAG] + else: + tag = None + return self.default_client.get_downloading_progress(tag=tag) + + def get_torrents(self, torrent_ids): + """ + 根据ID或状态查询下载器中的种子信息 + :param torrent_ids: 种子ID列表 + :return: 客户端类型,种子信息列表, 是否发生异常 + """ + if not self.default_client: + return None, [], True + torrent_list, _ = self.default_client.get_torrents(ids=torrent_ids) + return self._default_client_type, torrent_list + + def start_torrents(self, downloader=None, ids=None): + """ + 下载控制:开始 + :param downloader: 下载器类型 + :param ids: 种子ID列表 + :return: 处理状态 + """ + if not ids: + return False + if not downloader: + if not self.default_client: + return False + return self.default_client.start_torrents(ids) + else: + _client = self.__get_client(downloader) + return _client.start_torrents(ids) + + def stop_torrents(self, downloader=None, ids=None): + """ + 下载控制:停止 + :param downloader: 下载器类型 + :param ids: 种子ID列表 + :return: 处理状态 + """ + if not ids: + return False + if not downloader: + if not self.default_client: + return False + return self.default_client.stop_torrents(ids) + else: + _client = self.__get_client(downloader) + return _client.stop_torrents(ids) + + def delete_torrents(self, downloader=None, ids=None, delete_file=False): + """ + 删除种子 + :param downloader: 下载器类型 + :param ids: 种子ID列表 + :param delete_file: 是否删除文件 + :return: 处理状态 + """ + if not ids: + return False + if not downloader: + if not self.default_client: + return False + return self.default_client.delete_torrents(delete_file=delete_file, ids=ids) + else: + _client = self.__get_client(downloader) + return _client.delete_torrents(delete_file=delete_file, ids=ids) + + def batch_download(self, + in_from: SearchType, + media_list: list, + need_tvs: dict = None, + user_name=None): + """ + 根据命中的种子媒体信息,添加下载,由RSS或Searcher调用 + :param in_from: 来源 + :param media_list: 命中并已经识别好的媒体信息列表,包括名称、年份、季、集等信息 + :param need_tvs: 缺失的剧集清单,对于剧集只有在该清单中的季和集才会下载,对于电影无需输入该参数 + :param user_name: 用户名称 + :return: 已经添加了下载的媒体信息表表、剩余未下载到的媒体信息 + """ + + # 已下载的项目 + return_items = [] + # 返回按季、集数倒序排序的列表 + download_list = self.get_download_list(media_list) + + def __download(download_item, torrent_file=None, tag=None, is_paused=None): + """ + 下载及发送通知 + """ + download_item.user_name = user_name + state, msg = self.download( + media_info=download_item, + download_dir=download_item.save_path, + download_setting=download_item.download_setting, + torrent_file=torrent_file, + tag=tag, + is_paused=is_paused) + if state: + if download_item not in return_items: + return_items.append(download_item) + self.message.send_download_message(in_from, download_item) + else: + self.message.send_download_fail_message(download_item, msg) + return state + + def __update_seasons(tmdbid, need, current): + """ + 更新need_tvs季数 + """ + need = list(set(need).difference(set(current))) + for cur in current: + for nt in need_tvs.get(tmdbid): + if cur == nt.get("season") or (cur == 1 and not nt.get("season")): + need_tvs[tmdbid].remove(nt) + if not need_tvs.get(tmdbid): + need_tvs.pop(tmdbid) + return need + + def __update_episodes(tmdbid, seq, need, current): + """ + 更新need_tvs集数 + """ + need = list(set(need).difference(set(current))) + if need: + need_tvs[tmdbid][seq]["episodes"] = need + else: + need_tvs[tmdbid].pop(seq) + if not need_tvs.get(tmdbid): + need_tvs.pop(tmdbid) + return need + + def __get_season_episodes(tmdbid, season): + """ + 获取需要的季的集数 + """ + if not need_tvs.get(tmdbid): + return 0 + for nt in need_tvs.get(tmdbid): + if season == nt.get("season"): + return nt.get("total_episodes") + return 0 + + # 下载掉所有的电影 + for item in download_list: + if item.type == MediaType.MOVIE: + __download(item) + + # 电视剧整季匹配 + if need_tvs: + # 先把整季缺失的拿出来,看是否刚好有所有季都满足的种子 + need_seasons = {} + for need_tmdbid, need_tv in need_tvs.items(): + for tv in need_tv: + if not tv: + continue + if not tv.get("episodes"): + if not need_seasons.get(need_tmdbid): + need_seasons[need_tmdbid] = [] + need_seasons[need_tmdbid].append(tv.get("season") or 1) + # 查找整季包含的种子,只处理整季没集的种子或者是集数超过季的种子 + for need_tmdbid, need_season in need_seasons.items(): + for item in download_list: + if item.type == MediaType.MOVIE: + continue + item_season = item.get_season_list() + if item.get_episode_list(): + continue + if need_tmdbid == item.tmdb_id: + if set(item_season).issubset(set(need_season)): + if len(item_season) == 1: + # 只有一季的可能是命名错误,需要打开种子鉴别,只有实际集数大于等于总集数才下载 + torrent_episodes, torrent_path = self.get_torrent_episodes( + url=item.enclosure, + page_url=item.page_url) + if not torrent_episodes \ + or len(torrent_episodes) >= __get_season_episodes(need_tmdbid, item_season[0]): + download_state = __download(download_item=item, torrent_file=torrent_path) + else: + log.info( + f"【Downloader】种子 {item.org_string} 未含集数信息,解析文件数为 {len(torrent_episodes)}") + continue + else: + download_state = __download(item) + if download_state: + # 更新仍需季集 + need_season = __update_seasons(tmdbid=need_tmdbid, + need=need_season, + current=item_season) + # 电视剧季内的集匹配 + if need_tvs: + need_tv_list = list(need_tvs) + for need_tmdbid in need_tv_list: + need_tv = need_tvs.get(need_tmdbid) + if not need_tv: + continue + index = 0 + for tv in need_tv: + need_season = tv.get("season") or 1 + need_episodes = tv.get("episodes") + total_episodes = tv.get("total_episodes") + # 缺失整季的转化为缺失集进行比较 + if not need_episodes: + need_episodes = list(range(1, total_episodes + 1)) + for item in download_list: + if item.type == MediaType.MOVIE: + continue + if item.tmdb_id == need_tmdbid: + if item in return_items: + continue + item_season = item.get_season_list() + if len(item_season) != 1 or item_season[0] != need_season: + continue + item_episodes = item.get_episode_list() + if not item_episodes: + continue + # 只处理单季含集的种子,从集最多的开始下 + if set(item_episodes).issubset(set(need_episodes)): + if __download(item): + # 更新仍需集数 + need_episodes = __update_episodes(tmdbid=need_tmdbid, + need=need_episodes, + seq=index, + current=item_episodes) + index += 1 + + # 仍然缺失的剧集,从整季中选择需要的集数文件下载,仅支持QB和TR + if need_tvs: + need_tv_list = list(need_tvs) + for need_tmdbid in need_tv_list: + need_tv = need_tvs.get(need_tmdbid) + if not need_tv: + continue + index = 0 + for tv in need_tv: + need_season = tv.get("season") or 1 + need_episodes = tv.get("episodes") + if not need_episodes: + continue + for item in download_list: + if item.type == MediaType.MOVIE: + continue + if item in return_items: + continue + if not need_episodes: + break + # 选中一个单季整季的或单季包括需要的所有集的 + if item.tmdb_id == need_tmdbid \ + and (not item.get_episode_list() + or set(item.get_episode_list()).intersection(set(need_episodes))) \ + and len(item.get_season_list()) == 1 \ + and item.get_season_list()[0] == need_season: + # 检查种子看是否有需要的集 + torrent_episodes, torrent_path = self.get_torrent_episodes( + url=item.enclosure, + page_url=item.page_url) + selected_episodes = set(torrent_episodes).intersection(set(need_episodes)) + if not selected_episodes: + log.info("【Downloader】%s 没有需要的集,跳过..." % item.org_string) + continue + # 添加下载并暂停 + torrent_tag = "NT" + StringUtils.generate_random_str(5) + ret = __download(download_item=item, + torrent_file=torrent_path, + tag=torrent_tag, + is_paused=True) + if not ret: + continue + # 更新仍需集数 + need_episodes = __update_episodes(tmdbid=need_tmdbid, + need=need_episodes, + seq=index, + current=selected_episodes) + # 获取下载器 + downloader = self._default_client_type + if item.download_setting: + download_attr = self.get_download_setting(item.download_setting) + if download_attr.get("downloader"): + downloader = self.__get_client_type(download_attr.get("downloader")) + _client = self.__get_client(downloader) + # 获取刚添加的任务ID + if downloader == DownloaderType.TR: + torrent_id = ret.id + elif downloader == DownloaderType.QB: + torrent_id = _client.get_torrent_id_by_tag(tag=torrent_tag, status=["paused"]) + else: + continue + if not torrent_id: + log.error("【Downloader】获取下载器添加的任务信息出错:%s,tag=%s" % ( + item.org_string, torrent_tag)) + continue + # 设置任务只下载想要的文件 + log.info("【Downloader】从 %s 中选取集:%s" % (item.org_string, selected_episodes)) + self.set_files_status(torrent_id, selected_episodes, downloader) + # 重新开始任务 + log.info("【Downloader】%s 开始下载 " % item.org_string) + _client.start_torrents(torrent_id) + # 记录下载项 + return_items.append(item) + index += 1 + + # 返回下载的资源,剩下没下完的 + return return_items, need_tvs + + def check_exists_medias(self, meta_info, no_exists=None, total_ep=None): + """ + 检查媒体库,查询是否存在,对于剧集同时返回不存在的季集信息 + :param meta_info: 已识别的媒体信息,包括标题、年份、季、集信息 + :param no_exists: 在调用该方法前已经存储的不存在的季集信息,有传入时该函数检索的内容将会叠加后输出 + :param total_ep: 各季的总集数 + :return: 当前媒体是否缺失,各标题总的季集和缺失的季集,需要发送的消息 + """ + if not no_exists: + no_exists = {} + if not total_ep: + total_ep = {} + # 查找的季 + if not meta_info.begin_season: + search_season = None + else: + search_season = meta_info.get_season_list() + # 查找的集 + search_episode = meta_info.get_episode_list() + if search_episode and not search_season: + search_season = [1] + + # 返回的消息列表 + message_list = [] + if meta_info.type != MediaType.MOVIE: + # 是否存在的标志 + return_flag = False + # 检索电视剧的信息 + tv_info = self.media.get_tmdb_info(mtype=MediaType.TV, tmdbid=meta_info.tmdb_id) + if tv_info: + # 传入检查季 + total_seasons = [] + if search_season: + for season in search_season: + if total_ep.get(season): + episode_num = total_ep.get(season) + else: + episode_num = self.media.get_tmdb_season_episodes_num(tv_info=tv_info, season=season) + if not episode_num: + log.info("【Downloader】%s 第%s季 不存在" % (meta_info.get_title_string(), season)) + message_list.append("%s 第%s季 不存在" % (meta_info.get_title_string(), season)) + continue + total_seasons.append({"season_number": season, "episode_count": episode_num}) + log.info( + "【Downloader】%s 第%s季 共有 %s 集" % (meta_info.get_title_string(), season, episode_num)) + else: + # 共有多少季,每季有多少季 + total_seasons = self.media.get_tmdb_tv_seasons(tv_info=tv_info) + log.info( + "【Downloader】%s %s 共有 %s 季" % ( + meta_info.type.value, meta_info.get_title_string(), len(total_seasons))) + message_list.append( + "%s %s 共有 %s 季" % (meta_info.type.value, meta_info.get_title_string(), len(total_seasons))) + # 没有得到总季数时,返回None + if not total_seasons: + return_flag = None + else: + # 查询缺少多少集 + for season in total_seasons: + season_number = season.get("season_number") + episode_count = season.get("episode_count") + if not season_number or not episode_count: + continue + # 检查Emby + no_exists_episodes = self.mediaserver.get_no_exists_episodes(meta_info, + season_number, + episode_count) + # 没有配置Emby + if no_exists_episodes is None: + no_exists_episodes = self.filetransfer.get_no_exists_medias(meta_info, + season_number, + episode_count) + if no_exists_episodes: + # 排序 + no_exists_episodes.sort() + # 缺失集初始化 + if not no_exists.get(meta_info.tmdb_id): + no_exists[meta_info.tmdb_id] = [] + # 缺失集提示文本 + exists_tvs_str = "、".join(["%s" % tv for tv in no_exists_episodes]) + # 存入总缺失集 + if len(no_exists_episodes) >= episode_count: + no_item = {"season": season_number, "episodes": [], "total_episodes": episode_count} + log.info( + "【Downloader】%s 第%s季 缺失 %s 集" % ( + meta_info.get_title_string(), season_number, episode_count)) + if search_season: + message_list.append( + "%s 第%s季 缺失 %s 集" % (meta_info.title, season_number, episode_count)) + else: + message_list.append("第%s季 缺失 %s 集" % (season_number, episode_count)) + else: + no_item = {"season": season_number, "episodes": no_exists_episodes, + "total_episodes": episode_count} + log.info( + "【Downloader】%s 第%s季 缺失集:%s" % ( + meta_info.get_title_string(), season_number, exists_tvs_str)) + if search_season: + message_list.append( + "%s 第%s季 缺失集:%s" % (meta_info.title, season_number, exists_tvs_str)) + else: + message_list.append("第%s季 缺失集:%s" % (season_number, exists_tvs_str)) + if no_item not in no_exists.get(meta_info.tmdb_id): + no_exists[meta_info.tmdb_id].append(no_item) + # 输入检查集 + if search_episode: + # 有集数,肯定只有一季 + if not set(search_episode).intersection(set(no_exists_episodes)): + # 搜索的跟不存在的没有交集,说明都存在了 + msg = f"媒体库中已存在剧集:\n" \ + f" • {meta_info.get_title_string()} {meta_info.get_season_episode_string()}" + log.info(f"【Downloader】{msg}") + message_list.append(msg) + return_flag = True + break + else: + log.info("【Downloader】%s 第%s季 共%s集 已全部存在" % ( + meta_info.get_title_string(), season_number, episode_count)) + if search_season: + message_list.append( + "%s 第%s季 共%s集 已全部存在" % (meta_info.title, season_number, episode_count)) + else: + message_list.append( + "第%s季 共%s集 已全部存在" % (season_number, episode_count)) + else: + log.info("【Downloader】%s 无法查询到媒体详细信息" % meta_info.get_title_string()) + message_list.append("%s 无法查询到媒体详细信息" % meta_info.get_title_string()) + return_flag = None + # 全部存在 + if return_flag is False and not no_exists.get(meta_info.tmdb_id): + return_flag = True + # 返回 + return return_flag, no_exists, message_list + # 检查电影 + else: + exists_movies = self.mediaserver.get_movies(meta_info.title, meta_info.year) + if exists_movies is None: + exists_movies = self.filetransfer.get_no_exists_medias(meta_info) + if exists_movies: + movies_str = "\n • ".join(["%s (%s)" % (m.get('title'), m.get('year')) for m in exists_movies]) + msg = f"媒体库中已存在电影:\n • {movies_str}" + log.info(f"【Downloader】{msg}") + message_list.append(msg) + return True, {}, message_list + return False, {}, message_list + + def set_files_status(self, tid, need_episodes, downloader): + """ + 设置文件下载状态,选中需要下载的季集对应的文件下载,其余不下载 + :param tid: 种子的hash或id + :param need_episodes: 需要下载的文件的集信息 + :param downloader: 下载器 + :return: 返回选中的集的列表 + """ + sucess_epidised = [] + _client = self.__get_client(downloader) + if downloader == DownloaderType.TR: + files_info = {} + torrent_files = _client.get_files(tid) + if not torrent_files: + return [] + for file_id, torrent_file in enumerate(torrent_files): + meta_info = MetaInfo(torrent_file.name) + if not meta_info.get_episode_list(): + selected = False + else: + selected = set(meta_info.get_episode_list()).issubset(set(need_episodes)) + if selected: + sucess_epidised = list(set(sucess_epidised).union(set(meta_info.get_episode_list()))) + if not files_info.get(tid): + files_info[tid] = {file_id: {'priority': 'normal', 'selected': selected}} + else: + files_info[tid][file_id] = {'priority': 'normal', 'selected': selected} + if sucess_epidised and files_info: + _client.set_files(file_info=files_info) + elif downloader == DownloaderType.QB: + file_ids = [] + torrent_files = _client.get_files(tid) + if not torrent_files: + return [] + for torrent_file in torrent_files: + meta_info = MetaInfo(torrent_file.get("name")) + if not meta_info.get_episode_list() or not set(meta_info.get_episode_list()).issubset( + set(need_episodes)): + file_ids.append(torrent_file.get("index")) + else: + sucess_epidised = list(set(sucess_epidised).union(set(meta_info.get_episode_list()))) + if sucess_epidised and file_ids: + _client.set_files(torrent_hash=tid, file_ids=file_ids, priority=0) + return sucess_epidised + + def get_download_list(self, media_list): + """ + 对媒体信息进行排序、去重 + """ + if not media_list: + return [] + + # 排序函数,标题、站点、资源类型、做种数量 + def get_sort_str(x): + season_len = str(len(x.get_season_list())).rjust(2, '0') + episode_len = str(len(x.get_episode_list())).rjust(4, '0') + # 排序:标题、资源类型、站点、做种、季集 + if self._download_order == "seeder": + return "%s%s%s%s%s" % (str(x.title).ljust(100, ' '), + str(x.res_order).rjust(3, '0'), + str(x.seeders).rjust(10, '0'), + str(x.site_order).rjust(3, '0'), + "%s%s" % (season_len, episode_len)) + else: + return "%s%s%s%s%s" % (str(x.title).ljust(100, ' '), + str(x.res_order).rjust(3, '0'), + str(x.site_order).rjust(3, '0'), + str(x.seeders).rjust(10, '0'), + "%s%s" % (season_len, episode_len)) + + # 匹配的资源中排序分组选最好的一个下载 + # 按站点顺序、资源匹配顺序、做种人数下载数逆序排序 + media_list = sorted(media_list, key=lambda x: get_sort_str(x), reverse=True) + # 控重 + can_download_list_item = [] + can_download_list = [] + # 排序后重新加入数组,按真实名称控重,即只取每个名称的第一个 + for t_item in media_list: + # 控重的主链是名称、年份、季、集 + if t_item.type != MediaType.MOVIE: + media_name = "%s%s" % (t_item.get_title_string(), + t_item.get_season_episode_string()) + else: + media_name = t_item.get_title_string() + if media_name not in can_download_list: + can_download_list.append(media_name) + can_download_list_item.append(t_item) + return can_download_list_item + + def get_download_dirs(self, setting=None): + """ + 返回下载器中设置的保存目录 + """ + if not self._downloaddir: + return [] + if not setting: + setting = self.get_default_download_setting() + # 查询下载设置 + download_setting = self.get_download_setting(sid=setting) + # 下载设置为QB + if download_setting \ + and download_setting.get('downloader') == "Qbittorrent" \ + and Config().get_config("qbittorrent").get("auto_management"): + return [] + # 默认下载器为QB + if download_setting \ + and not download_setting.get('downloader') \ + and Config().get_config("pt").get("pt_client") == "qbittorrent" \ + and Config().get_config("qbittorrent").get("auto_management"): + return [] + # 查询目录 + save_path_list = [attr.get("save_path") for attr in self._downloaddir if attr.get("save_path")] + save_path_list.sort() + return list(set(save_path_list)) + + def get_download_visit_dirs(self): + """ + 返回下载器中设置的访问目录 + """ + if not self._downloaddir: + return [] + visit_path_list = [attr.get("container_path") or attr.get("save_path") for attr in self._downloaddir if + attr.get("save_path")] + visit_path_list.sort() + return list(set(visit_path_list)) + + def get_download_visit_dir(self, download_dir): + """ + 返回下载器中设置的访问目录 + """ + if not self.default_client: + return "" + return self.default_client.get_replace_path(download_dir) + + def __get_download_dir_info(self, media): + """ + 根据媒体信息读取一个下载目录的信息 + """ + if media and media.tmdb_info: + for attr in self._downloaddir or []: + if not attr: + continue + if attr.get("type") and attr.get("type") != media.type.value: + continue + if attr.get("category") and attr.get("category") != media.category: + continue + if not attr.get("save_path") and not attr.get("label"): + continue + if (attr.get("container_path") or attr.get("save_path")) \ + and os.path.exists(attr.get("container_path") or attr.get("save_path")) \ + and media.size \ + and float(SystemUtils.get_free_space_gb(attr.get("container_path") or attr.get("save_path"))) \ + < float(int(StringUtils.num_filesize(media.size)) / 1024 / 1024 / 1024): + continue + return {"path": attr.get("save_path"), "label": attr.get("label")} + return {"path": None, "label": None} + + def get_default_client_type(self): + """ + 返回下载器类型 + """ + return self._default_client_type + + @staticmethod + def __get_client_type(type_name): + """ + 根据名称返回下载器类型 + """ + if not type_name: + return None + for dict_type in DownloaderType: + if dict_type.name == type_name or dict_type.value == type_name: + return dict_type + + def get_torrent_episodes(self, url, page_url=None): + """ + 解析种子文件,获取集数 + :return: 集数列表、种子路径 + """ + site_info = self.sites.get_site_attr(url) + # 保存种子文件 + file_path, _, _, files, retmsg = Torrent().get_torrent_info( + url=url, + cookie=site_info.get("cookie"), + ua=site_info.get("ua"), + referer=page_url if site_info.get("referer") else None, + proxy=site_info.get("proxy") + ) + if not files: + log.error("【Downloader】读取种子文件集数出错:%s" % retmsg) + return [], None + episodes = [] + for file in files: + if os.path.splitext(file)[-1] not in RMT_MEDIAEXT: + continue + meta = MetaInfo(file) + if not meta.begin_episode: + continue + episodes = list(set(episodes).union(set(meta.get_episode_list()))) + return episodes, file_path + + def get_download_setting(self, sid=None): + """ + 获取下载设置 + :return: 下载设置 + """ + if sid: + return self._download_setting.get(str(sid)) + else: + return self._download_setting + + def get_default_download_setting(self): + """ + 获取默认下载设置 + :return: 默认下载设置id + """ + default_download_setting = SystemConfig().get_system_config("DefaultDownloadSetting") or "-1" + if not self._download_setting.get(default_download_setting): + default_download_setting = "-1" + return default_download_setting + + def set_speed_limit(self, downloader, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not downloader: + return [] + _client = self.__get_client(downloader) + try: + download_limit = int(download_limit) if download_limit else 0 + except Exception as err: + ExceptionUtils.exception_traceback(err) + download_limit = 0 + try: + upload_limit = int(upload_limit) if upload_limit else 0 + except Exception as err: + ExceptionUtils.exception_traceback(err) + upload_limit = 0 + _client.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) diff --git a/app/filetransfer.py b/app/filetransfer.py new file mode 100644 index 0000000..b8815fc --- /dev/null +++ b/app/filetransfer.py @@ -0,0 +1,1315 @@ +import argparse +import os +import random +import re +import shutil +import traceback +from enum import Enum +from threading import Lock +from time import sleep + +import log +from app.conf import ModuleConf +from app.helper import DbHelper, ProgressHelper +from app.helper import ThreadHelper +from app.media import Media, Category, Scraper +from app.media.meta import MetaInfo +from app.mediaserver import MediaServer +from app.message import Message +from app.subtitle import Subtitle +from app.utils import EpisodeFormat, PathUtils, StringUtils, SystemUtils, ExceptionUtils +from app.utils.types import MediaType, SyncType, RmtMode +from config import RMT_SUBEXT, RMT_MEDIAEXT, RMT_FAVTYPE, RMT_MIN_FILESIZE, DEFAULT_MOVIE_FORMAT, \ + DEFAULT_TV_FORMAT, Config + +lock = Lock() + + +class FileTransfer: + media = None + message = None + category = None + mediaserver = None + scraper = None + threadhelper = None + dbhelper = None + progress = None + + _default_rmt_mode = None + _movie_path = None + _tv_path = None + _anime_path = None + _movie_category_flag = None + _tv_category_flag = None + _anime_category_flag = None + _unknown_path = None + _min_filesize = RMT_MIN_FILESIZE + _filesize_cover = False + _movie_dir_rmt_format = "" + _movie_file_rmt_format = "" + _tv_dir_rmt_format = "" + _tv_season_rmt_format = "" + _tv_file_rmt_format = "" + _scraper_flag = False + _scraper_nfo = {} + _scraper_pic = {} + _refresh_mediaserver = False + _ignored_paths = [] + _ignored_files = '' + + def __init__(self): + self.media = Media() + self.message = Message() + self.category = Category() + self.mediaserver = MediaServer() + self.scraper = Scraper() + self.threadhelper = ThreadHelper() + self.dbhelper = DbHelper() + self.progress = ProgressHelper() + self.init_config() + + def init_config(self): + media = Config().get_config('media') + self._scraper_flag = media.get("nfo_poster") + self._scraper_nfo = Config().get_config('scraper_nfo') + self._scraper_pic = Config().get_config('scraper_pic') + if media: + # 刷新媒体库开关 + self._refresh_mediaserver = media.get("refresh_mediaserver") + # 电影目录 + self._movie_path = media.get('movie_path') + if not isinstance(self._movie_path, list): + if self._movie_path: + self._movie_path = [self._movie_path] + else: + self._movie_path = [] + # 电影分类 + self._movie_category_flag = self.category.get_movie_category_flag() + # 电视剧目录 + self._tv_path = media.get('tv_path') + if not isinstance(self._tv_path, list): + if self._tv_path: + self._tv_path = [self._tv_path] + else: + self._tv_path = [] + # 电视剧分类 + self._tv_category_flag = self.category.get_tv_category_flag() + # 动漫目录 + self._anime_path = media.get('anime_path') + if not isinstance(self._anime_path, list): + if self._anime_path: + self._anime_path = [self._anime_path] + else: + self._anime_path = [] + # 动漫分类 + self._anime_category_flag = self.category.get_anime_category_flag() + # 没有动漫目漫切换为电视剧目录和分类 + if not self._anime_path: + self._anime_path = self._tv_path + self._anime_category_flag = self._tv_category_flag + # 未识别目录 + self._unknown_path = media.get('unknown_path') + if not isinstance(self._unknown_path, list): + if self._unknown_path: + self._unknown_path = [self._unknown_path] + else: + self._unknown_path = [] + # 最小文件大小 + min_filesize = media.get('min_filesize') + if isinstance(min_filesize, int): + self._min_filesize = min_filesize * 1024 * 1024 + elif isinstance(min_filesize, str) and min_filesize.isdigit(): + self._min_filesize = int(min_filesize) * 1024 * 1024 + # 文件路径转移忽略词 + ignored_paths = media.get('ignored_paths') + if ignored_paths: + if ignored_paths.endswith(";"): + ignored_paths = ignored_paths[:-1] + self._ignored_paths = re.compile(r'%s' % re.sub(r';', r'|', ignored_paths)) + # 文件名转移忽略词 + ignored_files = media.get('ignored_files') + if ignored_files: + if ignored_files.endswith(";"): + ignored_files = ignored_files[:-1] + self._ignored_files = re.compile(r'%s' % re.sub(r';', r'|', ignored_files)) + # 高质量文件覆盖 + self._filesize_cover = media.get('filesize_cover') + # 电影重命名格式 + movie_name_format = media.get('movie_name_format') or DEFAULT_MOVIE_FORMAT + movie_formats = movie_name_format.rsplit('/', 1) + if movie_formats: + self._movie_dir_rmt_format = movie_formats[0] + if len(movie_formats) > 1: + self._movie_file_rmt_format = movie_formats[-1] + # 电视剧重命名格式 + tv_name_format = media.get('tv_name_format') or DEFAULT_TV_FORMAT + tv_formats = tv_name_format.rsplit('/', 2) + if tv_formats: + self._tv_dir_rmt_format = tv_formats[0] + if len(tv_formats) > 2: + self._tv_season_rmt_format = tv_formats[-2] + self._tv_file_rmt_format = tv_formats[-1] + self._default_rmt_mode = ModuleConf.RMT_MODES.get(Config().get_config('pt').get('rmt_mode', 'copy'), + RmtMode.COPY) + + @staticmethod + def __transfer_command(file_item, target_file, rmt_mode): + """ + 使用系统命令处理单个文件 + :param file_item: 文件路径 + :param target_file: 目标文件路径 + :param rmt_mode: RmtMode转移方式 + """ + with lock: + if rmt_mode == RmtMode.LINK: + # 更链接 + retcode, retmsg = SystemUtils.link(file_item, target_file) + elif rmt_mode == RmtMode.SOFTLINK: + # 软链接 + retcode, retmsg = SystemUtils.softlink(file_item, target_file) + elif rmt_mode == RmtMode.MOVE: + # 移动 + retcode, retmsg = SystemUtils.move(file_item, target_file) + elif rmt_mode == RmtMode.RCLONE: + # Rclone移动 + retcode, retmsg = SystemUtils.rclone_move(file_item, target_file) + elif rmt_mode == RmtMode.RCLONECOPY: + # Rclone复制 + retcode, retmsg = SystemUtils.rclone_copy(file_item, target_file) + elif rmt_mode == RmtMode.MINIO: + # Minio移动 + retcode, retmsg = SystemUtils.minio_move(file_item, target_file) + elif rmt_mode == RmtMode.MINIOCOPY: + # Minio复制 + retcode, retmsg = SystemUtils.minio_copy(file_item, target_file) + else: + # 复制 + retcode, retmsg = SystemUtils.copy(file_item, target_file) + if retcode != 0: + log.error("【Rmt】%s" % retmsg) + return retcode + + def __transfer_subtitles(self, org_name, new_name, rmt_mode): + """ + 根据文件名转移对应字幕文件 + :param org_name: 原文件名 + :param new_name: 新文件名 + :param rmt_mode: RmtMode转移方式 + """ + # 字幕正则式 + _zhcn_sub_re = r"([.\[(](((zh[-_])?(cn|ch[si]|sg|sc))|zho?" \ + r"|chinese|(cn|ch[si]|sg|zho?|eng)[-_&](cn|ch[si]|sg|zho?|eng)" \ + r"|简[体中]?)[.\])])" \ + r"|([\u4e00-\u9fa5]{0,3}[中双][\u4e00-\u9fa5]{0,2}[字文语][\u4e00-\u9fa5]{0,3})" \ + r"|简体|简中" + _zhtw_sub_re = r"([.\[(](((zh[-_])?(hk|tw|cht|tc))" \ + r"|繁[体中]?)[.\])])" \ + r"|繁体中[文字]|中[文字]繁体|繁体" + _eng_sub_re = r"[.\[(]eng[.\])]" + + # 比对文件名并转移字幕 + dir_name = os.path.dirname(org_name) + file_name = os.path.basename(org_name) + file_list = PathUtils.get_dir_level1_files(dir_name, RMT_SUBEXT) + if len(file_list) == 0: + log.debug("【Rmt】%s 目录下没有找到字幕文件..." % dir_name) + else: + log.debug("【Rmt】字幕文件清单:" + str(file_list)) + metainfo = MetaInfo(title=file_name) + for file_item in file_list: + sub_file_name = re.sub(_zhtw_sub_re, + ".", + re.sub(_zhcn_sub_re, + ".", + os.path.basename(file_item), + flags=re.I), + flags=re.I) + sub_file_name = re.sub(_eng_sub_re, ".", sub_file_name, flags=re.I) + sub_metainfo = MetaInfo(title=os.path.basename(file_item)) + if (os.path.splitext(file_name)[0] == os.path.splitext(sub_file_name)[0]) or \ + (sub_metainfo.cn_name and sub_metainfo.cn_name == metainfo.cn_name) \ + or (sub_metainfo.en_name and sub_metainfo.en_name == metainfo.en_name): + if metainfo.get_season_string() \ + and metainfo.get_season_string() != sub_metainfo.get_season_string(): + continue + if metainfo.get_episode_string() \ + and metainfo.get_episode_string() != sub_metainfo.get_episode_string(): + continue + new_file_type = "" + # 兼容jellyfin字幕识别(多重识别), emby则会识别最后一个后缀 + if re.search(_zhcn_sub_re, file_item, re.I): + new_file_type = ".chi.zh-cn" + elif re.search(_zhtw_sub_re, file_item, + re.I): + new_file_type = ".zh-tw" + elif re.search(_eng_sub_re, file_item, re.I): + new_file_type = ".eng" + # 通过对比字幕文件大小 尽量转移所有存在的字幕 + file_ext = os.path.splitext(file_item)[-1] + new_sub_tag_dict = { + ".eng": ".英文", + ".chi.zh-cn": ".简体中文", + ".zh-tw": ".繁体中文" + } + new_sub_tag_list = [ + new_file_type if t == 0 else "%s%s(%s)" % (new_file_type, + new_sub_tag_dict.get( + new_file_type, "" + ), + t) for t in range(6) + ] + for new_sub_tag in new_sub_tag_list: + new_file = os.path.splitext(new_name)[0] + new_sub_tag + file_ext + # 如果字幕文件不存在, 直接转移字幕, 并跳出循环 + try: + if not os.path.exists(new_file): + log.debug("【Rmt】正在处理字幕:%s" % os.path.basename(file_item)) + retcode = self.__transfer_command(file_item=file_item, + target_file=new_file, + rmt_mode=rmt_mode) + if retcode == 0: + log.info("【Rmt】字幕 %s %s完成" % (os.path.basename(file_item), rmt_mode.value)) + break + else: + log.error( + "【Rmt】字幕 %s %s失败,错误码 %s" % (file_name, rmt_mode.value, str(retcode))) + return retcode + # 如果字幕文件的大小与已存在文件相同, 说明已经转移过了, 则跳出循环 + elif os.path.getsize(new_file) == os.path.getsize(file_item): + log.info("【Rmt】字幕 %s 已存在" % new_file) + break + # 否则 循环继续 > 通过new_sub_tag_list 获取新的tag附加到字幕文件名, 继续检查是否能转移 + except OSError as reason: + log.info("【Rmt】字幕 %s 出错了,原因: %s" % (new_file, str(reason))) + return 0 + + def __transfer_bluray_dir(self, file_path, new_path, rmt_mode): + """ + 转移蓝光文件夹 + :param file_path: 原路径 + :param new_path: 新路径 + :param rmt_mode: RmtMode转移方式 + """ + log.info("【Rmt】正在%s目录:%s 到 %s" % (rmt_mode.value, file_path, new_path)) + # 复制 + retcode = self.__transfer_dir_files(src_dir=file_path, + target_dir=new_path, + rmt_mode=rmt_mode, + bludir=True) + if retcode == 0: + log.info("【Rmt】文件 %s %s完成" % (file_path, rmt_mode.value)) + else: + log.error("【Rmt】文件%s %s失败,错误码 %s" % (file_path, rmt_mode.value, str(retcode))) + return retcode + + def is_target_dir_path(self, path): + """ + 判断是否为目的路径下的路径 + :param path: 路径 + :return: True/False + """ + if not path: + return False + for tv_path in self._tv_path: + if PathUtils.is_path_in_path(tv_path, path): + return True + for movie_path in self._movie_path: + if PathUtils.is_path_in_path(movie_path, path): + return True + for anime_path in self._anime_path: + if PathUtils.is_path_in_path(anime_path, path): + return True + for unknown_path in self._unknown_path: + if PathUtils.is_path_in_path(unknown_path, path): + return True + return False + + def __transfer_dir_files(self, src_dir, target_dir, rmt_mode, bludir=False): + """ + 按目录结构转移所有文件 + :param src_dir: 原路径 + :param target_dir: 新路径 + :param rmt_mode: RmtMode转移方式 + :param bludir: 是否蓝光目录 + """ + file_list = PathUtils.get_dir_files(src_dir) + retcode = 0 + for file in file_list: + new_file = file.replace(src_dir, target_dir) + if os.path.exists(new_file): + log.warn("【Rmt】%s 文件已存在" % new_file) + continue + new_dir = os.path.dirname(new_file) + if not os.path.exists(new_dir): + os.makedirs(new_dir) + retcode = self.__transfer_command(file_item=file, + target_file=new_file, + rmt_mode=rmt_mode) + if retcode != 0: + break + else: + if not bludir: + self.dbhelper.insert_transfer_blacklist(file) + if retcode == 0 and bludir: + self.dbhelper.insert_transfer_blacklist(src_dir) + return retcode + + def __transfer_origin_file(self, file_item, target_dir, rmt_mode): + """ + 按原文件名link文件到目的目录 + :param file_item: 原文件路径 + :param target_dir: 目的目录 + :param rmt_mode: RmtMode转移方式 + """ + if not file_item or not target_dir: + return -1 + if not os.path.exists(file_item): + log.warn("【Rmt】%s 不存在" % file_item) + return -1 + # 计算目录目录 + parent_name = os.path.basename(os.path.dirname(file_item)) + target_dir = os.path.join(target_dir, parent_name) + if not os.path.exists(target_dir): + log.debug("【Rmt】正在创建目录:%s" % target_dir) + os.makedirs(target_dir) + # 目录 + if os.path.isdir(file_item): + log.info("【Rmt】正在%s目录:%s 到 %s" % (rmt_mode.value, file_item, target_dir)) + retcode = self.__transfer_dir_files(src_dir=file_item, + target_dir=target_dir, + rmt_mode=rmt_mode) + # 文件 + else: + target_file = os.path.join(target_dir, os.path.basename(file_item)) + if os.path.exists(target_file): + log.warn("【Rmt】%s 文件已存在" % target_file) + return 0 + retcode = self.__transfer_command(file_item=file_item, + target_file=target_file, + rmt_mode=rmt_mode) + if retcode == 0: + self.dbhelper.insert_transfer_blacklist(file_item) + if retcode == 0: + log.info("【Rmt】%s %s到unknown完成" % (file_item, rmt_mode.value)) + else: + log.error("【Rmt】%s %s到unknown失败,错误码 %s" % (file_item, rmt_mode.value, retcode)) + return retcode + + def __transfer_file(self, file_item, new_file, rmt_mode, over_flag=False, old_file=None): + """ + 转移一个文件,同时处理字幕 + :param file_item: 原文件路径 + :param new_file: 新文件路径 + :param rmt_mode: RmtMode转移方式 + :param over_flag: 是否覆盖,为True时会先删除再转移 + """ + file_name = os.path.basename(file_item) + if not over_flag and os.path.exists(new_file): + log.warn("【Rmt】文件已存在:%s" % new_file) + return 0 + if over_flag and old_file and os.path.isfile(old_file): + log.info("【Rmt】正在删除已存在的文件:%s" % old_file) + os.remove(old_file) + log.info("【Rmt】正在转移文件:%s 到 %s" % (file_name, new_file)) + retcode = self.__transfer_command(file_item=file_item, + target_file=new_file, + rmt_mode=rmt_mode) + if retcode == 0: + log.info("【Rmt】文件 %s %s完成" % (file_name, rmt_mode.value)) + self.dbhelper.insert_transfer_blacklist(file_item) + else: + log.error("【Rmt】文件 %s %s失败,错误码 %s" % (file_name, rmt_mode.value, str(retcode))) + return retcode + # 处理字幕 + return self.__transfer_subtitles(org_name=file_item, + new_name=new_file, + rmt_mode=rmt_mode) + + def transfer_media(self, + in_from: Enum, + in_path, + rmt_mode: RmtMode = None, + files: list = None, + target_dir=None, + unknown_dir=None, + tmdb_info=None, + media_type: MediaType = None, + season=None, + episode: (EpisodeFormat, bool) = None, + min_filesize=None, + udf_flag=False, + root_path=False): + """ + 识别并转移一个文件、多个文件或者目录 + :param in_from: 来源,即调用该功能的渠道 + :param in_path: 转移的路径,可能是一个文件也可以是一个目录 + :param files: 文件清单,非空时以该文件清单为准,为空时从in_path中按后缀和大小限制检索需要处理的文件清单 + :param target_dir: 目的文件夹,非空的转移到该文件夹,为空时则按类型转移到配置文件中的媒体库文件夹 + :param unknown_dir: 未识别文件夹,非空时未识别的媒体文件转移到该文件夹,为空时则使用配置文件中的未识别文件夹 + :param rmt_mode: 文件转移方式 + :param tmdb_info: 手动识别转移时传入的TMDB信息对象,如未输入,则按名称笔TMDB实时查询 + :param media_type: 手动识别转移时传入的文件类型,如未输入,则自动识别 + :param season: 手动识别目录或文件时传入的的字号,如未输入,则自动识别 + :param episode: (EpisodeFormat,是否批处理匹配) + :param min_filesize: 过滤小文件大小的上限值 + :param udf_flag: 自定义转移标志,为True时代表是自定义转移,此时很多处理不一样 + :param root_path: 是否根目录下的文件 + :return: 处理状态,错误信息 + """ + + def __finish_transfer(status, message): + if status: + self.progress.update(ptype="filetransfer", + value=100, + text=f"{in_path} 转移成功!") + else: + self.progress.update(ptype="filetransfer", + value=100, + text=f"{in_path} 转移失败:{message}!") + self.progress.end('filetransfer') + return status, message + + # 开始进度 + self.progress.start('filetransfer') + + episode = (None, False) if not episode else episode + if not in_path: + log.error("【Rmt】输入路径错误!") + return __finish_transfer(False, "输入路径错误") + + if not rmt_mode: + rmt_mode = self._default_rmt_mode + + log.info("【Rmt】开始处理:%s,转移方式:%s" % (in_path, rmt_mode.value)) + + success_flag = True + error_message = "" + bluray_disk_dir = None + if not files: + # 如果传入的是个目录 + if os.path.isdir(in_path): + if not os.path.exists(in_path): + log.error("【Rmt】文件转移失败,目录不存在 %s" % in_path) + return __finish_transfer(False, "目录不存在") + # 回收站及隐藏的文件不处理 + if PathUtils.is_invalid_path(in_path): + return __finish_transfer(False, "回收站或者隐藏文件夹") + # 判断是不是原盘文件夹 + bluray_disk_dir = PathUtils.get_bluray_dir(in_path) + if bluray_disk_dir: + file_list = [bluray_disk_dir] + log.info("【Rmt】当前为蓝光原盘文件夹:%s" % str(in_path)) + else: + if str(min_filesize) == "0": + # 不限制大小 + now_filesize = 0 + else: + # 未输入大小限制默认为配置大小限制 + now_filesize = self._min_filesize if not str(min_filesize).isdigit() else int( + min_filesize) * 1024 * 1024 + # 查找目录下的文件 + file_list = PathUtils.get_dir_files(in_path=in_path, + episode_format=episode[0], + exts=RMT_MEDIAEXT, + filesize=now_filesize) + log.debug("【Rmt】文件清单:" + str(file_list)) + if len(file_list) == 0: + log.warn("【Rmt】%s 目录下未找到媒体文件,当前最小文件大小限制为 %s" + % (in_path, StringUtils.str_filesize(now_filesize))) + return __finish_transfer(False, + "目录下未找到媒体文件,当前最小文件大小限制为 %s" + % StringUtils.str_filesize(now_filesize)) + # 传入的是个文件 + else: + if not os.path.exists(in_path): + log.error("【Rmt】文件转移失败,文件不存在:%s" % in_path) + return __finish_transfer(False, "文件不存在") + if os.path.splitext(in_path)[-1].lower() not in RMT_MEDIAEXT: + log.warn("【Rmt】不支持的媒体文件格式,不处理:%s" % in_path) + return __finish_transfer(False, "不支持的媒体文件格式") + # 判断是不是原盘文件夹 + bluray_disk_dir = PathUtils.get_bluray_dir(in_path) + if bluray_disk_dir: + file_list = [bluray_disk_dir] + log.info("【Rmt】当前为蓝光原盘文件夹:%s" % bluray_disk_dir) + else: + file_list = [in_path] + else: + # 传入的是个文件列表,这些文失件是in_path下面的文件 + file_list = files + + # 过滤掉文件列表 + file_list, msg = self.check_ignore(file_list=file_list) + if not file_list: + return __finish_transfer(True, msg) + + # 目录同步模式下,过滤掉文件列表中已处理过的 + if in_from == SyncType.MON: + file_list = list(filter(self.dbhelper.is_transfer_notin_blacklist, file_list)) + if not file_list: + log.info("【Rmt】所有文件均已成功转移过,没有需要处理的文件!如需重新处理,请清理缓存(服务->清理转移缓存)") + return __finish_transfer(True, "没有新文件需要处理") + # API检索出媒体信息,传入一个文件列表,得出每一个文件的名称,这里是当前目录下所有的文件了 + Medias = self.media.get_media_info_on_files(file_list, tmdb_info, media_type, season, episode[0]) + if not Medias: + log.error("【Rmt】检索媒体信息出错!") + return __finish_transfer(False, "检索媒体信息出错") + + # 更新进度 + self.progress.update(ptype="filetransfer", text=f"共 {len(Medias)} 个文件需要处理...") + + # 统计总的文件数、失败文件数、需要提醒的失败数 + failed_count = 0 + alert_count = 0 + alert_messages = [] + total_count = 0 + # 电视剧可能有多集,如果在循环里发消息就太多了,要在外面发消息 + message_medias = {} + # 需要刷新媒体库的清单 + refresh_library_items = [] + # 需要下载字段的清单 + download_subtitle_items = [] + # 处理识别后的每一个文件或单个文件夹 + for file_item, media in Medias.items(): + try: + # 总数量 + total_count = total_count + 1 + + if not udf_flag: + if re.search(r'[./\s\[]+Sample[/.\s\]]+', file_item, re.IGNORECASE): + log.warn("【Rmt】%s 可能是预告片,跳过..." % file_item) + continue + + # 文件名 + file_name = os.path.basename(file_item) + # 更新进度 + self.progress.update(ptype="filetransfer", + value=round(total_count/len(Medias) * 100) - (0.5/len(Medias) * 100), + text="正在处理:%s ..." % file_name) + + # 数据库记录的路径 + if bluray_disk_dir: + reg_path = bluray_disk_dir + else: + reg_path = file_item + # 未识别 + if not media or not media.tmdb_info or not media.get_title_string(): + log.warn("【Rmt】%s 无法识别媒体信息!" % file_name) + success_flag = False + error_message = "无法识别媒体信息" + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + # 记录未识别 + is_need_insert_unknown = self.dbhelper.is_need_insert_transfer_unknown(reg_path) + if is_need_insert_unknown: + self.dbhelper.insert_transfer_unknown(reg_path, target_dir, rmt_mode) + alert_count += 1 + failed_count += 1 + if error_message not in alert_messages and is_need_insert_unknown: + alert_messages.append(error_message) + # 原样转移过去 + if unknown_dir: + log.warn("【Rmt】%s 按原文件名转移到未识别目录:%s" % (file_name, unknown_dir)) + self.__transfer_origin_file(file_item=file_item, target_dir=unknown_dir, rmt_mode=rmt_mode) + elif self._unknown_path: + unknown_path = self.__get_best_unknown_path(in_path) + if not unknown_path: + continue + log.warn("【Rmt】%s 按原文件名转移到未识别目录:%s" % (file_name, unknown_path)) + self.__transfer_origin_file(file_item=file_item, target_dir=unknown_path, rmt_mode=rmt_mode) + else: + log.error("【Rmt】%s 无法识别媒体信息!" % file_name) + continue + # 当前文件大小 + media.size = os.path.getsize(file_item) + # 目的目录,有输入target_dir时,往这个目录放 + if target_dir: + dist_path = target_dir + else: + dist_path = self.__get_best_target_path(mtype=media.type, in_path=in_path, size=media.size) + if not dist_path: + log.error("【Rmt】文件转移失败,目的路径不存在!") + success_flag = False + error_message = "目的路径不存在" + failed_count += 1 + alert_count += 1 + if error_message not in alert_messages: + alert_messages.append(error_message) + continue + if dist_path and not os.path.exists(dist_path): + return __finish_transfer(False, "目录不存在:%s" % dist_path) + + # 判断文件是否已存在,返回:目录存在标志、目录名、文件存在标志、文件名 + dir_exist_flag, ret_dir_path, file_exist_flag, ret_file_path = self.__is_media_exists(dist_path, media) + # 新文件后缀 + file_ext = os.path.splitext(file_item)[-1] + new_file = ret_file_path + # 已存在的文件数量 + exist_filenum = 0 + handler_flag = False + # 路径存在 + if dir_exist_flag: + # 蓝光原盘 + if bluray_disk_dir: + log.warn("【Rmt】蓝光原盘目录已存在:%s" % ret_dir_path) + if udf_flag: + return __finish_transfer(False, "蓝光原盘目录已存在:%s" % ret_dir_path) + failed_count += 1 + continue + # 文件存在 + if file_exist_flag: + exist_filenum = exist_filenum + 1 + if rmt_mode != RmtMode.SOFTLINK: + if media.size > os.path.getsize(ret_file_path) and self._filesize_cover or udf_flag: + ret_file_path, ret_file_ext = os.path.splitext(ret_file_path) + new_file = "%s%s" % (ret_file_path, file_ext) + old_file = "%s%s" % (ret_file_path, ret_file_ext) + log.info("【Rmt】文件 %s 已存在,覆盖为 %s" % (old_file, new_file)) + ret = self.__transfer_file(file_item=file_item, + new_file=new_file, + rmt_mode=rmt_mode, + over_flag=True, old_file=old_file) + if ret != 0: + success_flag = False + error_message = "文件转移失败,错误码 %s" % ret + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + failed_count += 1 + alert_count += 1 + if error_message not in alert_messages: + alert_messages.append(error_message) + continue + handler_flag = True + else: + log.warn("【Rmt】文件 %s 已存在" % ret_file_path) + failed_count += 1 + continue + else: + log.warn("【Rmt】文件 %s 已存在" % ret_file_path) + failed_count += 1 + continue + # 路径不存在 + else: + if not ret_dir_path: + log.error("【Rmt】拼装目录路径错误,无法从文件名中识别出季集信息:%s" % file_item) + success_flag = False + error_message = "识别失败,无法从文件名中识别出季集信息" + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + # 记录未识别 + is_need_insert_unknown = self.dbhelper.is_need_insert_transfer_unknown(reg_path) + if is_need_insert_unknown: + self.dbhelper.insert_transfer_unknown(reg_path, target_dir, rmt_mode) + alert_count += 1 + failed_count += 1 + if error_message not in alert_messages and is_need_insert_unknown: + alert_messages.append(error_message) + continue + else: + # 创建电录 + log.debug("【Rmt】正在创建目录:%s" % ret_dir_path) + os.makedirs(ret_dir_path) + # 转移蓝光原盘 + if bluray_disk_dir: + ret = self.__transfer_bluray_dir(file_item, ret_dir_path, rmt_mode) + if ret != 0: + success_flag = False + error_message = "蓝光目录转移失败,错误码:%s" % ret + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + failed_count += 1 + alert_count += 1 + if error_message not in alert_messages: + alert_messages.append(error_message) + continue + else: + # 开始转移文件 + if not handler_flag: + if not ret_file_path: + log.error("【Rmt】拼装文件路径错误,无法从文件名中识别出集数:%s" % file_item) + success_flag = False + error_message = "识别失败,无法从文件名中识别出集数" + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + # 记录未识别 + is_need_insert_unknown = self.dbhelper.is_need_insert_transfer_unknown(reg_path) + if is_need_insert_unknown: + self.dbhelper.insert_transfer_unknown(reg_path, target_dir, rmt_mode) + alert_count += 1 + failed_count += 1 + if error_message not in alert_messages and is_need_insert_unknown: + alert_messages.append(error_message) + continue + new_file = "%s%s" % (ret_file_path, file_ext) + ret = self.__transfer_file(file_item=file_item, + new_file=new_file, + rmt_mode=rmt_mode, + over_flag=False) + if ret != 0: + success_flag = False + error_message = "文件转移失败,错误码 %s" % ret + self.progress.update(ptype="filetransfer", text=error_message) + if udf_flag: + return __finish_transfer(success_flag, error_message) + failed_count += 1 + alert_count += 1 + if error_message not in alert_messages: + alert_messages.append(error_message) + continue + # 媒体库刷新条目:类型-类别-标题-年份 + refresh_item = {"type": media.type, "category": media.category, "title": media.title, + "year": media.year, "target_path": dist_path} + # 登记媒体库刷新 + if refresh_item not in refresh_library_items: + refresh_library_items.append(refresh_item) + # 查询TMDB详情,需要全部数据 + media.set_tmdb_info(self.media.get_tmdb_info(mtype=media.type, + tmdbid=media.tmdb_id, + append_to_response="all")) + # 下载字幕条目 + subtitle_item = {"type": media.type, + "file": ret_file_path, + "file_ext": os.path.splitext(file_item)[-1], + "name": media.en_name if media.en_name else media.cn_name, + "title": media.title, + "year": media.year, + "season": media.begin_season, + "episode": media.begin_episode, + "bluray": True if bluray_disk_dir else False, + "imdbid": media.imdb_id} + # 登记字幕下载 + if subtitle_item not in download_subtitle_items: + download_subtitle_items.append(subtitle_item) + # 转移历史记录 + self.dbhelper.insert_transfer_history( + in_from=in_from, + rmt_mode=rmt_mode, + in_path=reg_path, + out_path=new_file if not bluray_disk_dir else None, + dest=dist_path, + media_info=media) + # 未识别手动识别或历史记录重新识别的批处理模式 + if isinstance(episode[1], bool) and episode[1]: + # 未识别手动识别,更改未识别记录为已处理 + self.dbhelper.update_transfer_unknown_state(file_item) + # 电影立即发送消息 + if media.type == MediaType.MOVIE: + self.message.send_transfer_movie_message(in_from, + media, + exist_filenum, + self._movie_category_flag) + # 否则登记汇总发消息 + else: + # 按季汇总 + message_key = "%s-%s" % (media.get_title_string(), media.get_season_string()) + if not message_medias.get(message_key): + message_medias[message_key] = media + # 汇总集数、大小 + if not message_medias[message_key].is_in_episode(media.get_episode_list()): + message_medias[message_key].total_episodes += media.total_episodes + message_medias[message_key].size += media.size + # 生成nfo及poster + if self._scraper_flag: + # 生成刮削文件 + self.scraper.gen_scraper_files(media=media, + scraper_nfo=self._scraper_nfo, + scraper_pic=self._scraper_pic, + dir_path=ret_dir_path, + file_name=os.path.basename(ret_file_path), + file_ext=file_ext) + # 更新进度 + self.progress.update(ptype="filetransfer", + value=round(total_count / len(Medias) * 100), + text="%s 转移完成" % file_name) + # 移动模式随机休眠(兼容一些网盘挂载目录) + if rmt_mode == RmtMode.MOVE: + sleep(round(random.uniform(0, 1), 1)) + + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【Rmt】文件转移时发生错误:%s - %s" % (str(err), traceback.format_exc())) + # 循环结束 + # 统计完成情况,发送通知 + if message_medias: + self.message.send_transfer_tv_message(message_medias, in_from) + # 刷新媒体库 + if refresh_library_items and self._refresh_mediaserver: + self.mediaserver.refresh_library_by_items(refresh_library_items) + # 启新进程下载字幕 + if download_subtitle_items: + self.threadhelper.start_thread(Subtitle().download_subtitle, (download_subtitle_items,)) + # 总结 + log.info("【Rmt】%s 处理完成,总数:%s,失败:%s" % (in_path, total_count, failed_count)) + if alert_count > 0: + self.message.send_transfer_fail_message(in_path, alert_count, "、".join(alert_messages)) + elif failed_count == 0: + # 删除空目录 + if rmt_mode == RmtMode.MOVE \ + and os.path.exists(in_path) \ + and os.path.isdir(in_path) \ + and not root_path \ + and not PathUtils.get_dir_files(in_path=in_path, exts=RMT_MEDIAEXT) \ + and not PathUtils.get_dir_files(in_path=in_path, exts=['.!qb', '.part']): + log.info("【Rmt】目录下已无媒体文件及正在下载的文件,移动模式下删除目录:%s" % in_path) + shutil.rmtree(in_path) + return __finish_transfer(success_flag, error_message) + + def transfer_manually(self, s_path, t_path, mode): + """ + 全量转移,用于使用命令调用 + :param s_path: 源目录 + :param t_path: 目的目录 + :param mode: 转移方式 + """ + if not s_path: + return + if not os.path.exists(s_path): + print("【Rmt】源目录不存在:%s" % s_path) + return + if t_path: + if not os.path.exists(t_path): + print("【Rmt】目的目录不存在:%s" % t_path) + return + rmt_mode = ModuleConf.RMT_MODES.get(mode) + if not rmt_mode: + print("【Rmt】转移模式错误!") + return + print("【Rmt】转移模式为:%s" % rmt_mode.value) + print("【Rmt】正在转移以下目录中的全量文件:%s" % s_path) + for path in PathUtils.get_dir_level1_medias(s_path, RMT_MEDIAEXT): + if PathUtils.is_invalid_path(path): + continue + ret, ret_msg = self.transfer_media(in_from=SyncType.MAN, + in_path=path, + target_dir=t_path, + rmt_mode=rmt_mode) + if not ret: + print("【Rmt】%s 处理失败:%s" % (path, ret_msg)) + + def __is_media_exists(self, + media_dest, + media): + """ + 判断媒体文件是否忆存在 + :param media_dest: 媒体文件所在目录 + :param media: 已识别的媒体信息 + :return: 目录是否存在,目录路径,文件是否存在,文件路径 + """ + # 返回变量 + dir_exist_flag = False + file_exist_flag = False + ret_dir_path = None + ret_file_path = None + # 电影 + if media.type == MediaType.MOVIE: + # 目录名称 + dir_name, file_name = self.get_moive_dest_path(media) + # 默认目录路径 + file_path = os.path.join(media_dest, dir_name) + # 开启分类时目录路径 + if self._movie_category_flag: + file_path = os.path.join(media_dest, media.category, dir_name) + for m_type in [RMT_FAVTYPE, media.category]: + type_path = os.path.join(media_dest, m_type, dir_name) + # 目录是否存在 + if os.path.exists(type_path): + file_path = type_path + break + # 返回路径 + ret_dir_path = file_path + # 路径存在标志 + if os.path.exists(file_path): + dir_exist_flag = True + # 文件路径 + file_dest = os.path.join(file_path, file_name) + # 返回文件路径 + ret_file_path = file_dest + # 文件是否存在 + for ext in RMT_MEDIAEXT: + ext_dest = "%s%s" % (file_dest, ext) + if os.path.exists(ext_dest): + file_exist_flag = True + ret_file_path = ext_dest + break + # 电视剧或者动漫 + else: + # 目录名称 + dir_name, season_name, file_name = self.get_tv_dest_path(media) + # 剧集目录 + if (media.type == MediaType.TV and self._tv_category_flag) or ( + media.type == MediaType.ANIME and self._anime_category_flag): + media_path = os.path.join(media_dest, media.category, dir_name) + else: + media_path = os.path.join(media_dest, dir_name) + # 季 + if media.get_season_list(): + # 季路径 + season_dir = os.path.join(media_path, season_name) + # 返回目录路径 + ret_dir_path = season_dir + # 目录是否存在 + if os.path.exists(season_dir): + dir_exist_flag = True + # 处理集 + episodes = media.get_episode_list() + if episodes: + # 集文件路径 + file_path = os.path.join(season_dir, file_name) + # 返回文件路径 + ret_file_path = file_path + # 文件存在标志 + for ext in RMT_MEDIAEXT: + ext_dest = "%s%s" % (file_path, ext) + if os.path.exists(ext_dest): + file_exist_flag = True + ret_file_path = ext_dest + break + return dir_exist_flag, ret_dir_path, file_exist_flag, ret_file_path + + def transfer_embyfav(self, item_path): + """ + Emby/Jellyfin点红星后转移电影文件到精选分类 + :param item_path: 文件路径 + """ + if not item_path: + return False + if not self._movie_category_flag or not self._movie_path: + return False + if os.path.isdir(item_path): + movie_dir = item_path + else: + movie_dir = os.path.dirname(item_path) + # 已经是精选下的不处理 + movie_type = os.path.basename(os.path.dirname(movie_dir)) + if movie_type == RMT_FAVTYPE \ + or movie_type not in self.category.get_movie_categorys(): + return False + movie_name = os.path.basename(movie_dir) + movie_path = self.__get_best_target_path(mtype=MediaType.MOVIE, in_path=movie_dir) + # 开始转移文件,转移到同目录下的精选目录 + org_path = os.path.join(movie_path, movie_type, movie_name) + new_path = os.path.join(movie_path, RMT_FAVTYPE, movie_name) + if os.path.exists(org_path): + log.info("【Rmt】开始转移文件 %s 到 %s ..." % (org_path, new_path)) + if os.path.exists(new_path): + log.info("【Rmt】目录 %s 已存在" % new_path) + return False + ret, retmsg = SystemUtils.move(org_path, new_path) + if ret == 0: + return True + else: + log.error("【Rmt】%s" % retmsg) + else: + log.error("【Rmt】%s 目录不存在" % org_path) + return False + + def get_dest_path_by_info(self, dest, meta_info): + """ + 拼装转移重命名后的新文件地址 + :param dest: 目的目录 + :param meta_info: 媒体信息 + """ + if not dest or not meta_info: + return None + if meta_info.type == MediaType.MOVIE: + dir_name, _ = self.get_moive_dest_path(meta_info) + if self._movie_category_flag: + return os.path.join(dest, meta_info.category, dir_name) + else: + return os.path.join(dest, dir_name) + else: + dir_name, season_name, _ = self.get_tv_dest_path(meta_info) + if self._tv_category_flag: + return os.path.join(dest, meta_info.category, dir_name, season_name) + else: + return os.path.join(dest, dir_name, season_name) + + def get_no_exists_medias(self, meta_info, season=None, total_num=None): + """ + 根据媒体库目录结构,判断媒体是否存在 + :param meta_info: 已识别的媒体信息 + :param season: 季号,数字,剧集时需要 + :param total_num: 该季总集数,剧集时需要 + :return: 如果是电影返回已存在的电影清单:title、year,如果是剧集,则返回不存在的集的清单 + """ + # 电影 + if meta_info.type == MediaType.MOVIE: + dir_name, _ = self.get_moive_dest_path(meta_info) + for dest_path in self._movie_path: + # 判断精选 + fav_path = os.path.join(dest_path, RMT_FAVTYPE, dir_name) + fav_files = PathUtils.get_dir_files(fav_path, RMT_MEDIAEXT) + # 其它分类 + if self._movie_category_flag: + dest_path = os.path.join(dest_path, meta_info.category, dir_name) + else: + dest_path = os.path.join(dest_path, dir_name) + files = PathUtils.get_dir_files(dest_path, RMT_MEDIAEXT) + if len(files) > 0 or len(fav_files) > 0: + return [{'title': meta_info.title, 'year': meta_info.year}] + return [] + # 电视剧 + else: + dir_name, season_name, _ = self.get_tv_dest_path(meta_info) + if not season or not total_num: + return [] + if meta_info.type == MediaType.ANIME: + dest_paths = self._anime_path + category_flag = self._anime_category_flag + else: + dest_paths = self._tv_path + category_flag = self._tv_category_flag + # 总需要的集 + total_episodes = [episode for episode in range(1, total_num + 1)] + # 已存在的集 + exists_episodes = [] + for dest_path in dest_paths: + if category_flag: + dest_path = os.path.join(dest_path, meta_info.category, dir_name, season_name) + else: + dest_path = os.path.join(dest_path, dir_name, season_name) + # 目录不存在 + if not os.path.exists(dest_path): + continue + files = PathUtils.get_dir_files(dest_path, RMT_MEDIAEXT) + for file in files: + file_meta_info = MetaInfo(os.path.basename(file)) + if not file_meta_info.get_season_list() or not file_meta_info.get_episode_list(): + continue + if file_meta_info.get_name() != meta_info.title: + continue + if not file_meta_info.is_in_season(season): + continue + exists_episodes = list(set(exists_episodes).union(set(file_meta_info.get_episode_list()))) + return list(set(total_episodes).difference(set(exists_episodes))) + + def __get_best_target_path(self, mtype, in_path=None, size=0): + """ + 查询一个最好的目录返回,有in_path时找与in_path同路径的,没有in_path时,顺序查找1个符合大小要求的,没有in_path和size时,返回第1个 + :param mtype: 媒体类型:电影、电视剧、动漫 + :param in_path: 源目录 + :param size: 文件大小 + """ + if not mtype: + return None + if mtype == MediaType.MOVIE: + dest_paths = self._movie_path + elif mtype == MediaType.TV: + dest_paths = self._tv_path + else: + dest_paths = self._anime_path + if not dest_paths: + return None + if not isinstance(dest_paths, list): + return dest_paths + if isinstance(dest_paths, list) and len(dest_paths) == 1: + return dest_paths[0] + # 有输入路径的,匹配有共同上级路径的 + if in_path: + # 先用自定义规则匹配 找同级目录最多的路径 + max_return_path = None + max_path_len = 0 + for dest_path in dest_paths: + try: + path_len = len(os.path.commonpath([in_path, dest_path])) + if path_len > max_path_len: + max_path_len = path_len + max_return_path = dest_path + except Exception as err: + ExceptionUtils.exception_traceback(err) + continue + if max_return_path: + return max_return_path + # 有输入大小的,匹配第1个满足空间存储要求的 + if size: + for path in dest_paths: + disk_free_size = SystemUtils.get_free_space_gb(path) + if float(disk_free_size) > float(size / 1024 / 1024 / 1024): + return path + # 默认返回第1个 + return dest_paths[0] + + def __get_best_unknown_path(self, in_path): + """ + 查找最合适的unknown目录 + :param in_path: 源目录 + """ + if not self._unknown_path: + return None + for unknown_path in self._unknown_path: + if os.path.commonpath([in_path, unknown_path]) not in ["/", "\\"]: + return unknown_path + return self._unknown_path[0] + + def link_sync_file(self, src_path, in_file, target_dir, sync_transfer_mode): + """ + 对文件做纯链接处理,不做识别重命名,则监控模块调用 + :param : 来源渠道 + :param src_path: 源目录 + :param in_file: 源文件 + :param target_dir: 目的目录 + :param sync_transfer_mode: 明确的转移方式 + """ + new_file = in_file.replace(src_path, target_dir) + new_file_list, msg = self.check_ignore(file_list=[new_file]) + if not new_file_list: + return 0, msg + else: + new_file = new_file_list[0] + new_dir = os.path.dirname(new_file) + if not os.path.exists(new_dir): + os.makedirs(new_dir) + return self.__transfer_command(file_item=in_file, + target_file=new_file, + rmt_mode=sync_transfer_mode), "" + + def get_format_dict(self, media): + """ + 根据媒体信息,返回Format字典 + """ + if not media: + return {} + episode_title = self.media.get_episode_title(media) + # 此处使用独立对象,避免影响语言 + en_title = Media().get_tmdb_en_title(media) + return { + "title": StringUtils.clear_file_name(media.title), + "en_title": StringUtils.clear_file_name(en_title), + "original_name": StringUtils.clear_file_name(os.path.splitext(media.org_string or "")[0]), + "original_title": StringUtils.clear_file_name(media.original_title), + "name": StringUtils.clear_file_name(media.get_name()), + "year": media.year, + "edition": media.get_edtion_string() or None, + "videoFormat": media.resource_pix, + "releaseGroup": media.resource_team, + "effect": media.resource_effect, + "videoCodec": media.video_encode, + "audioCodec": media.audio_encode, + "tmdbid": media.tmdb_id, + "season": media.get_season_seq(), + "episode": media.get_episode_seqs(), + "episode_title": StringUtils.clear_file_name(episode_title), + "season_episode": "%s%s" % (media.get_season_item(), media.get_episode_items()), + "part": media.part + } + + def get_moive_dest_path(self, media_info): + """ + 计算电影文件路径 + :return: 电影目录、电影名称 + """ + format_dict = self.get_format_dict(media_info) + dir_name = re.sub(r"[-_\s.]*None", "", self._movie_dir_rmt_format.format(**format_dict)) + file_name = re.sub(r"[-_\s.]*None", "", self._movie_file_rmt_format.format(**format_dict)) + return dir_name, file_name + + def get_tv_dest_path(self, media_info): + """ + 计算电视剧文件路径 + :return: 电视剧目录、季目录、集名称 + """ + format_dict = self.get_format_dict(media_info) + dir_name = re.sub(r"[-_\s.]*None", "", self._tv_dir_rmt_format.format(**format_dict)) + season_name = re.sub(r"[-_\s.]*None", "", self._tv_season_rmt_format.format(**format_dict)) + file_name = re.sub(r"[-_\s.]*None", "", self._tv_file_rmt_format.format(**format_dict)) + return dir_name, season_name, file_name + + def check_ignore(self, file_list): + """ + 检查过滤文件列表中忽略项目 + :param file_list: 文件路径列表 + """ + if not file_list: + return [], "" + # 过滤掉文件列表中文件路径包含文件路径转移忽略词的 + if self._ignored_paths: + try: + for file in file_list[:]: + if re.findall(self._ignored_paths, os.path.dirname(file)): + log.info(f"【Rmt】{file} 文件路径含转移忽略词,已忽略转移") + file_list.remove(file) + if not file_list: + return [], "排除文件路径转移忽略词后,没有新文件需要处理" + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【Rmt】文件路径转移忽略词设置有误:%s" % str(err)) + + # 过滤掉文件列表中文件名包含文件名转移忽略词的 + if self._ignored_files: + try: + for file in file_list[:]: + if re.findall(self._ignored_files, os.path.basename(file)): + log.info(f"【Rmt】{file} 文件名包含转移忽略词,已忽略转移") + file_list.remove(file) + if not file_list: + return [], "排除文件名转移忽略词后,没有新文件需要处理" + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【Rmt】文件名转移忽略词设置有误:%s" % str(err)) + + return file_list, "" + + def get_media_exists_flag(self, mtype, title, year, mediaid): + """ + 获取媒体存在标记:是否存在、是否订阅 + :param: mtype 媒体类型 + :param: title 媒体标题 + :param: year 媒体年份 + :param: mediaid TMDBID/DB:豆瓣ID/BG:Bangumi的ID + :return: 1-已订阅/2-已下载/0-不存在未订阅, RSSID + """ + if str(mediaid).isdigit(): + tmdbid = mediaid + else: + tmdbid = None + if mtype in ["MOV", "电影", MediaType.MOVIE]: + rssid = self.dbhelper.get_rss_movie_id(title=title, year=year, tmdbid=tmdbid) + else: + if not tmdbid: + meta_info = MetaInfo(title=title) + title = meta_info.get_name() + season = meta_info.get_season_string() + if season: + year = None + else: + season = None + rssid = self.dbhelper.get_rss_tv_id(title=title, year=year, season=season, tmdbid=tmdbid) + if rssid: + # 已订阅 + fav = "1" + elif MediaServer().check_item_exists(title=title, year=year, tmdbid=tmdbid): + # 已下载 + fav = "2" + else: + # 未订阅、未下载 + fav = "0" + return fav, rssid + + +if __name__ == "__main__": + """ + 手工转移时,使用命名行调用 + """ + Config().init_syspath() + + parser = argparse.ArgumentParser(description='文件转移工具') + parser.add_argument('-m', '--mode', dest='mode', required=True, + help='转移模式:link copy softlink move rclone rclonecopy minio miniocopy') + parser.add_argument('-s', '--source', dest='s_path', required=True, help='硬链接源目录路径') + parser.add_argument('-d', '--target', dest='t_path', required=False, help='硬链接目的目录路径') + args = parser.parse_args() + if os.environ.get('NASTOOL_CONFIG'): + print("【Rmt】配置文件地址:%s" % os.environ.get('NASTOOL_CONFIG')) + print("【Rmt】源目录路径:%s" % args.s_path) + if args.t_path: + print("【Rmt】目的目录路径:%s" % args.t_path) + else: + print("【Rmt】目的目录为配置文件中的电影、电视剧媒体库目录") + FileTransfer().transfer_manually(args.s_path, args.t_path, args.mode) + else: + print("【Rmt】未设置环境变量,请先设置 NASTOOL_CONFIG 环境变量为配置文件地址") diff --git a/app/filter.py b/app/filter.py new file mode 100644 index 0000000..9d6c9d8 --- /dev/null +++ b/app/filter.py @@ -0,0 +1,322 @@ +import re + +from app.conf import ModuleConf +from app.helper import DbHelper +from app.media.meta import ReleaseGroupsMatcher +from app.utils import StringUtils +from app.utils.commons import singleton +from app.utils.types import MediaType + + +@singleton +class Filter: + rg_matcher = None + dbhelper = None + _groups = [] + _rules = [] + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.rg_matcher = ReleaseGroupsMatcher() + self._groups = self.dbhelper.get_config_filter_group() + self._rules = self.dbhelper.get_config_filter_rule() + + def get_rule_groups(self, groupid=None, default=False): + """ + 获取所有规则组 + """ + ret_groups = [] + for group in self._groups: + group_info = { + "id": group.ID, + "name": group.GROUP_NAME, + "default": group.IS_DEFAULT, + "note": group.NOTE + } + if (groupid and str(groupid) == str(group.ID)) \ + or (default and group.IS_DEFAULT == "Y"): + return group_info + ret_groups.append(group_info) + if groupid or default: + return {} + return ret_groups + + def get_rule_infos(self): + """ + 获取所有的规则组及组内的规则 + """ + groups = self.get_rule_groups() + for group in groups: + group['rules'] = self.get_rules(group.get("id")) + return groups + + def get_rules(self, groupid, ruleid=None): + """ + 获取过滤规则 + """ + if not groupid: + return [] + ret_rules = [] + for rule in self._rules: + rule_info = { + "id": rule.ID, + "group": rule.GROUP_ID, + "name": rule.ROLE_NAME, + "pri": rule.PRIORITY or 0, + "include": rule.INCLUDE.split("\n") if rule.INCLUDE else [], + "exclude": rule.EXCLUDE.split("\n") if rule.EXCLUDE else [], + "size": rule.SIZE_LIMIT, + "free": rule.NOTE, + "free_text": { + "1.0 1.0": "普通", + "1.0 0.0": "免费", + "2.0 0.0": "2X免费" + }.get(rule.NOTE, "全部") if rule.NOTE else "" + } + if str(rule.GROUP_ID) == str(groupid) \ + and (not ruleid or int(ruleid) == rule.ID): + ret_rules.append(rule_info) + if ruleid: + return ret_rules[0] if ret_rules else {} + return ret_rules + + def get_rule_first_order(self, rulegroup): + """ + 获取规则的最高优先级 + """ + if not rulegroup: + rulegroup = self.get_rule_groups(default=True) + first_order = min([int(rule_info.get("pri")) for rule_info in self.get_rules(groupid=rulegroup)] or [0]) + return 100 - first_order + + def check_rules(self, meta_info, rulegroup=None): + """ + 检查种子是否匹配站点过滤规则:排除规则、包含规则,优先规则 + :param meta_info: 识别的信息 + :param rulegroup: 规则组ID + :return: 是否匹配,匹配的优先值,规则名称,值越大越优先 + """ + if not meta_info: + return False, 0, "" + # 为-1时不使用过滤规则 + if rulegroup and int(rulegroup) == -1: + return True, 0, "不过滤" + if meta_info.subtitle: + title = "%s %s" % (meta_info.org_string, meta_info.subtitle) + else: + title = meta_info.org_string + if not rulegroup: + rulegroup = self.get_rule_groups(default=True) + if not rulegroup: + return True, 0, "未配置过滤规则" + else: + rulegroup = self.get_rule_groups(groupid=rulegroup) + filters = self.get_rules(groupid=rulegroup.get("id")) + # 命中优先级 + order_seq = 0 + # 当前规则组是否命中 + group_match = True + for filter_info in filters: + # 当前规则是否命中 + rule_match = True + # 命中规则的序号 + order_seq = 100 - int(filter_info.get('pri')) + # 必须包括的项 + includes = filter_info.get('include') + if includes and rule_match: + include_flag = True + for include in includes: + if not include: + continue + if not re.search(r'%s' % include.strip(), title, re.IGNORECASE): + include_flag = False + break + if not include_flag: + rule_match = False + + # 不能包含的项 + excludes = filter_info.get('exclude') + if excludes and rule_match: + exclude_flag = False + exclude_count = 0 + for exclude in excludes: + if not exclude: + continue + exclude_count += 1 + if not re.search(r'%s' % exclude.strip(), title, re.IGNORECASE): + exclude_flag = True + if exclude_count > 0 and not exclude_flag: + rule_match = False + # 大小 + sizes = filter_info.get('size') + if sizes and rule_match and meta_info.size: + meta_info.size = StringUtils.num_filesize(meta_info.size) + if sizes.find(',') != -1: + sizes = sizes.split(',') + if sizes[0].isdigit(): + begin_size = int(sizes[0].strip()) + else: + begin_size = 0 + if sizes[1].isdigit(): + end_size = int(sizes[1].strip()) + else: + end_size = 0 + else: + begin_size = 0 + if sizes.isdigit(): + end_size = int(sizes.strip()) + else: + end_size = 0 + if meta_info.type == MediaType.MOVIE: + if not begin_size * 1024 ** 3 <= int(meta_info.size) <= end_size * 1024 ** 3: + rule_match = False + else: + if meta_info.total_episodes \ + and not begin_size * 1024 ** 3 <= int(meta_info.size) / int(meta_info.total_episodes) <= end_size * 1024 ** 3: + rule_match = False + + # 促销 + free = filter_info.get("free") + if free and meta_info.upload_volume_factor is not None and meta_info.download_volume_factor is not None: + ul_factor, dl_factor = free.split() + if float(ul_factor) > meta_info.upload_volume_factor \ + or float(dl_factor) < meta_info.download_volume_factor: + rule_match = False + + if rule_match: + return True, order_seq, rulegroup.get("name") + else: + group_match = False + if not group_match: + return False, 0, rulegroup.get("name") + return True, order_seq, rulegroup.get("name") + + def is_rule_free(self, rulegroup=None): + """ + 判断规则中是否需要Free检测 + """ + if not rulegroup: + rulegroup = self.get_rule_groups(default=True) + if not rulegroup: + return True, 0, "" + else: + rulegroup = self.get_rule_groups(groupid=rulegroup) + filters = self.get_rules(groupid=rulegroup.get("id")) + for filter_info in filters: + if filter_info.get("free"): + return True + return False + + @staticmethod + def is_torrent_match_sey(media_info, s_num, e_num, year_str): + """ + 种子名称关键字匹配 + :param media_info: 已识别的种子信息 + :param s_num: 要匹配的季号,为空则不匹配 + :param e_num: 要匹配的集号,为空则不匹配 + :param year_str: 要匹配的年份,为空则不匹配 + :return: 是否命中 + """ + if s_num: + if not media_info.get_season_list(): + return False + if not isinstance(s_num, list): + s_num = [s_num] + if not set(s_num).issuperset(set(media_info.get_season_list())): + return False + if e_num: + if not isinstance(e_num, list): + e_num = [e_num] + if not set(e_num).issuperset(set(media_info.get_episode_list())): + return False + if year_str: + if str(media_info.year) != str(year_str): + return False + return True + + def check_torrent_filter(self, + meta_info, + filter_args, + uploadvolumefactor=None, + downloadvolumefactor=None): + """ + 对种子进行过滤 + :param meta_info: 名称识别后的MetaBase对象 + :param filter_args: 过滤条件的字典 + :param uploadvolumefactor: 种子的上传因子 传空不过滤 + :param downloadvolumefactor: 种子的下载因子 传空不过滤 + :return: 是否匹配,匹配的优先值,匹配信息,值越大越优先 + """ + # 过滤质量 + if filter_args.get("restype"): + restype_re = ModuleConf.TORRENT_SEARCH_PARAMS["restype"].get(filter_args.get("restype")) + if not meta_info.get_edtion_string(): + return False, 0, f"{meta_info.org_string} 不符合质量 {filter_args.get('restype')} 要求" + if restype_re and not re.search(r"%s" % restype_re, meta_info.get_edtion_string(), re.I): + return False, 0, f"{meta_info.org_string} 不符合质量 {filter_args.get('restype')} 要求" + # 过滤分辨率 + if filter_args.get("pix"): + pix_re = ModuleConf.TORRENT_SEARCH_PARAMS["pix"].get(filter_args.get("pix")) + if not meta_info.resource_pix: + return False, 0, f"{meta_info.org_string} 不符合分辨率 {filter_args.get('pix')} 要求" + if pix_re and not re.search(r"%s" % pix_re, meta_info.resource_pix, re.I): + return False, 0, f"{meta_info.org_string} 不符合分辨率 {filter_args.get('pix')} 要求" + # 过滤制作组/字幕组 + if filter_args.get("team"): + team = filter_args.get("team") + if not meta_info.resource_team: + resource_team = self.rg_matcher.match( + title=meta_info.org_string, + groups=team) + if not resource_team: + return False, 0, f"{meta_info.org_string} 不符合制作组/字幕组 {team} 要求" + else: + meta_info.resource_team = resource_team + elif not re.search(r"%s" % team, meta_info.resource_team, re.I): + return False, 0, f"{meta_info.org_string} 不符合制作组/字幕组 {team} 要求" + # 过滤促销 + if filter_args.get("sp_state"): + ul_factor, dl_factor = filter_args.get("sp_state").split() + if uploadvolumefactor and ul_factor not in ("*", str(uploadvolumefactor)): + return False, 0, f"{meta_info.org_string} 不符合促销要求" + if downloadvolumefactor and dl_factor not in ("*", str(downloadvolumefactor)): + return False, 0, f"{meta_info.org_string} 不符合促销要求" + # 过滤包含 + if filter_args.get("include"): + include = filter_args.get("include") + if not re.search(r"%s" % include, meta_info.org_string, re.I): + return False, 0, f"{meta_info.org_string} 不符合包含 {include} 要求" + # 过滤排除 + if filter_args.get("exclude"): + exclude = filter_args.get("exclude") + if re.search(r"%s" % exclude, meta_info.org_string, re.I): + return False, 0, f"{meta_info.org_string} 不符合排除 {exclude} 要求" + # 过滤关键字 + if filter_args.get("key"): + key = filter_args.get("key") + if not re.search(r"%s" % key, meta_info.org_string, re.I): + return False, 0, f"{meta_info.org_string} 不符合 {key} 要求" + # 过滤过滤规则,-1表示不使用过滤规则,空则使用默认过滤规则 + if filter_args.get("rule"): + # 已设置默认规则 + match_flag, order_seq, rule_name = self.check_rules(meta_info, filter_args.get("rule")) + match_msg = "%s 大小:%s 促销:%s 不符合订阅/站点过滤规则 %s 要求" % ( + meta_info.org_string, + StringUtils.str_filesize(meta_info.size), + meta_info.get_volume_factor_string(), + rule_name + ) + return match_flag, order_seq, match_msg + else: + # 默认过滤规则 + match_flag, order_seq, rule_name = self.check_rules(meta_info) + match_msg = "%s 大小:%s 促销:%s 不符合默认过滤规则 %s 要求" % ( + meta_info.org_string, + StringUtils.str_filesize(meta_info.size), + meta_info.get_volume_factor_string(), + rule_name + ) + return match_flag, order_seq, match_msg diff --git a/app/helper/__init__.py b/app/helper/__init__.py new file mode 100644 index 0000000..f323800 --- /dev/null +++ b/app/helper/__init__.py @@ -0,0 +1,16 @@ +from .chrome_helper import ChromeHelper +from .indexer_helper import IndexerHelper, IndexerConf +from .meta_helper import MetaHelper +from .progress_helper import ProgressHelper +from .security_helper import SecurityHelper +from .thread_helper import ThreadHelper +from .db_helper import DbHelper +from .dict_helper import DictHelper +from .display_helper import DisplayHelper +from .site_helper import SiteHelper +from .ocr_helper import OcrHelper +from .opensubtitles import OpenSubtitles +from .words_helper import WordsHelper +from .submodule_helper import SubmoduleHelper +from .cookiecloud_helper import CookieCloudHelper +from .ffmpeg_helper import FfmpegHelper diff --git a/app/helper/chrome_helper.py b/app/helper/chrome_helper.py new file mode 100644 index 0000000..8c9817a --- /dev/null +++ b/app/helper/chrome_helper.py @@ -0,0 +1,240 @@ +import json +import os.path +import tempfile +import time +from functools import reduce +from threading import Lock + +import undetected_chromedriver as uc +from webdriver_manager.chrome import ChromeDriverManager + +from app.utils import SystemUtils, RequestUtils + +lock = Lock() + +driver_executable_path = None + + +class ChromeHelper(object): + _executable_path = None + + _chrome = None + _headless = False + + def __init__(self, headless=False): + + self._executable_path = SystemUtils.get_webdriver_path() or driver_executable_path + + if SystemUtils.is_windows(): + self._headless = False + elif not os.environ.get("NASTOOL_DISPLAY"): + self._headless = True + else: + self._headless = headless + + def init_driver(self): + if self._executable_path: + return + if not uc.find_chrome_executable(): + return + global driver_executable_path + driver_executable_path = ChromeDriverManager().install() + + @property + def browser(self): + with lock: + if not self._chrome: + self._chrome = self.__get_browser() + return self._chrome + + def get_status(self): + if not self._executable_path: + return False + if self._executable_path \ + and not os.path.exists(self._executable_path): + return False + if not uc.find_chrome_executable(): + return False + return True + + def __get_browser(self): + if not self.get_status(): + return None + options = uc.ChromeOptions() + options.add_argument('--disable-gpu') + options.add_argument('--no-sandbox') + options.add_argument('--ignore-certificate-errors') + options.add_argument('--disable-dev-shm-usage') + options.add_argument("--start-maximized") + options.add_argument("--disable-blink-features=AutomationControlled") + options.add_argument("--disable-extensions") + options.add_argument("--disable-plugins-discovery") + options.add_argument('--no-first-run') + options.add_argument('--no-service-autorun') + options.add_argument('--no-default-browser-check') + options.add_argument('--password-store=basic') + if self._headless: + options.add_argument('--headless') + prefs = { + "useAutomationExtension": False, + "profile.managed_default_content_settings.images": 2 if self._headless else 1, + "excludeSwitches": ["enable-automation"] + } + options.add_experimental_option("prefs", prefs) + chrome = ChromeWithPrefs(options=options, driver_executable_path=self._executable_path) + chrome.set_page_load_timeout(30) + return chrome + + def visit(self, url, ua=None, cookie=None, timeout=30): + if not self.browser: + return False + try: + if ua: + self._chrome.execute_cdp_cmd("Emulation.setUserAgentOverride", { + "userAgent": ua + }) + if timeout: + self._chrome.implicitly_wait(timeout) + self._chrome.get(url) + if cookie: + self._chrome.delete_all_cookies() + for cookie in RequestUtils.cookie_parse(cookie, array=True): + self._chrome.add_cookie(cookie) + self._chrome.get(url) + return True + except Exception as err: + print(str(err)) + return False + + def new_tab(self, url, ua=None, cookie=None): + if not self._chrome: + return False + # 新开一个标签页 + try: + self._chrome.switch_to.new_window('tab') + except Exception as err: + print(str(err)) + return False + # 访问URL + return self.visit(url=url, ua=ua, cookie=cookie) + + def close_tab(self): + try: + self._chrome.close() + self._chrome.switch_to.window(self._chrome.window_handles[0]) + except Exception as err: + print(str(err)) + return False + + def pass_cloudflare(self, waittime=10): + cloudflare = False + for i in range(0, waittime): + if self.get_title() != "Just a moment...": + cloudflare = True + break + time.sleep(1) + return cloudflare + + def execute_script(self, script): + if not self._chrome: + return False + try: + return self._chrome.execute_script(script) + except Exception as err: + print(str(err)) + + def get_title(self): + if not self._chrome: + return "" + return self._chrome.title + + def get_html(self): + if not self._chrome: + return "" + return self._chrome.page_source + + def get_cookies(self): + if not self._chrome: + return "" + cookie_str = "" + try: + for _cookie in self._chrome.get_cookies(): + if not _cookie: + continue + cookie_str += "%s=%s;" % (_cookie.get("name"), _cookie.get("value")) + except Exception as err: + print(str(err)) + return cookie_str + + def get_ua(self): + try: + return self._chrome.execute_script("return navigator.userAgent") + except Exception as err: + print(str(err)) + return None + + def quit(self): + if self._chrome: + self._chrome.close() + self._chrome.quit() + self._fixup_uc_pid_leak() + self._chrome = None + + def _fixup_uc_pid_leak(self): + """ + uc 在处理退出时为强制kill进程,没有调用wait,会导致出现僵尸进程,此处增加wait,确保系统正常回收 + :return: + """ + try: + # chromedriver 进程 + if hasattr(self._chrome, "service") and getattr(self._chrome.service, "process", None): + self._chrome.service.process.wait(3) + # chrome 进程 + os.waitpid(self._chrome.browser_pid, 0) + except Exception as e: + print(str(e)) + pass + + def __del__(self): + self.quit() + + +class ChromeWithPrefs(uc.Chrome): + def __init__(self, *args, options=None, **kwargs): + if options: + self._handle_prefs(options) + super().__init__(*args, options=options, **kwargs) + # remove the user_data_dir when quitting + self.keep_user_data_dir = False + + @staticmethod + def _handle_prefs(options): + if prefs := options.experimental_options.get("prefs"): + # turn a (dotted key, value) into a proper nested dict + def undot_key(key, value): + if "." in key: + key, rest = key.split(".", 1) + value = undot_key(rest, value) + return {key: value} + + # undot prefs dict keys + undot_prefs = reduce( + lambda d1, d2: {**d1, **d2}, # merge dicts + (undot_key(key, value) for key, value in prefs.items()), + ) + + # create a user_data_dir and add its path to the options + user_data_dir = os.path.normpath(tempfile.mkdtemp()) + options.add_argument(f"--user-data-dir={user_data_dir}") + + # create the preferences json file in its default directory + default_dir = os.path.join(user_data_dir, "Default") + os.mkdir(default_dir) + + prefs_file = os.path.join(default_dir, "Preferences") + with open(prefs_file, encoding="latin1", mode="w") as f: + json.dump(undot_prefs, f) + + # pylint: disable=protected-access + # remove the experimental_options to avoid an error + del options._experimental_options["prefs"] diff --git a/app/helper/cookiecloud_helper.py b/app/helper/cookiecloud_helper.py new file mode 100644 index 0000000..7c9b5ae --- /dev/null +++ b/app/helper/cookiecloud_helper.py @@ -0,0 +1,41 @@ +import json + +from app.utils import RequestUtils, StringUtils + + +class CookieCloudHelper(object): + _req = None + _server = None + _key = None + _password = None + + def __init__(self, server, key, password): + self._server = server + if self._server: + if not self._server.startswith("http"): + self._server = "http://%s" % self._server + if self._server.endswith("/"): + self._server = self._server[:-1] + self._key = key + self._password = password + self._req = RequestUtils(content_type="application/json") + + def download_data(self): + """ + 从CookieCloud下载数据 + """ + if not self._server or not self._key or not self._password: + return {}, "CookieCloud参数不正确" + req_url = "%s/get/%s" % (self._server, self._key) + ret = self._req.post_res(url=req_url, json={"password": self._password}) + if ret and ret.status_code == 200: + result = ret.json() + if not result: + return {}, "" + if result.get("cookie_data"): + return result.get("cookie_data"), "" + return result, "" + elif ret: + return {}, "同步CookieCloud失败,错误码:%s" % ret.status_code + else: + return {}, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确" diff --git a/app/helper/db_helper.py b/app/helper/db_helper.py new file mode 100644 index 0000000..7ce2bc1 --- /dev/null +++ b/app/helper/db_helper.py @@ -0,0 +1,2492 @@ +import datetime +import os.path +import time +import json +from enum import Enum +from sqlalchemy import cast, func + +from app.db import MainDb, DbPersist +from app.db.models import * +from app.utils import StringUtils +from app.utils.types import MediaType, RmtMode + + +class DbHelper: + _db = MainDb() + + @DbPersist(_db) + def insert_search_results(self, media_items: list, title=None, ident_flag=True): + """ + 将返回信息插入数据库 + """ + if not media_items: + return + data_list = [] + for media_item in media_items: + if media_item.type == MediaType.TV: + mtype = "TV" + elif media_item.type == MediaType.MOVIE: + mtype = "MOV" + else: + mtype = "ANI" + data_list.append( + SEARCHRESULTINFO( + TORRENT_NAME=media_item.org_string, + ENCLOSURE=media_item.enclosure, + DESCRIPTION=media_item.description, + TYPE=mtype if ident_flag else '', + TITLE=media_item.title if ident_flag else title, + YEAR=media_item.year if ident_flag else '', + SEASON=media_item.get_season_string() if ident_flag else '', + EPISODE=media_item.get_episode_string() if ident_flag else '', + ES_STRING=media_item.get_season_episode_string() if ident_flag else '', + VOTE=media_item.vote_average or "0", + IMAGE=media_item.get_backdrop_image(default=False, original=True), + POSTER=media_item.get_poster_image(), + TMDBID=media_item.tmdb_id, + OVERVIEW=media_item.overview, + RES_TYPE=json.dumps({ + "respix": media_item.resource_pix, + "restype": media_item.resource_type, + "reseffect": media_item.resource_effect, + "video_encode": media_item.video_encode + }), + RES_ORDER=media_item.res_order, + SIZE=StringUtils.str_filesize(int(media_item.size)), + SEEDERS=media_item.seeders, + PEERS=media_item.peers, + SITE=media_item.site, + SITE_ORDER=media_item.site_order, + PAGEURL=media_item.page_url, + OTHERINFO=media_item.resource_team, + UPLOAD_VOLUME_FACTOR=media_item.upload_volume_factor, + DOWNLOAD_VOLUME_FACTOR=media_item.download_volume_factor + )) + self._db.insert(data_list) + + def get_search_result_by_id(self, dl_id): + """ + 根据ID从数据库中查询检索结果的一条记录 + """ + return self._db.query(SEARCHRESULTINFO).filter(SEARCHRESULTINFO.ID == dl_id).all() + + def get_search_results(self, ): + """ + 查询检索结果的所有记录 + """ + return self._db.query(SEARCHRESULTINFO).all() + + def is_torrent_rssd(self, enclosure): + """ + 查询RSS是否处理过,根据下载链接 + """ + if not enclosure: + return True + if self._db.query(RSSTORRENTS).filter(RSSTORRENTS.ENCLOSURE == enclosure).count() > 0: + return True + else: + return False + + def is_userrss_finished(self, torrent_name, enclosure): + """ + 查询RSS是否处理过,根据名称 + """ + if not torrent_name and not enclosure: + return True + if enclosure: + ret = self._db.query(RSSTORRENTS).filter(RSSTORRENTS.ENCLOSURE == enclosure).count() + else: + ret = self._db.query(RSSTORRENTS).filter(RSSTORRENTS.TORRENT_NAME == torrent_name).count() + return True if ret > 0 else False + + @DbPersist(_db) + def delete_all_search_torrents(self, ): + """ + 删除所有搜索的记录 + """ + self._db.query(SEARCHRESULTINFO).delete() + + @DbPersist(_db) + def insert_rss_torrents(self, media_info): + """ + 将RSS的记录插入数据库 + """ + self._db.insert( + RSSTORRENTS( + TORRENT_NAME=media_info.org_string, + ENCLOSURE=media_info.enclosure, + TYPE=media_info.type.value, + TITLE=media_info.title, + YEAR=media_info.year, + SEASON=media_info.get_season_string(), + EPISODE=media_info.get_episode_string() + )) + + @DbPersist(_db) + def simple_insert_rss_torrents(self, title, enclosure): + """ + 将RSS的记录插入数据库 + """ + self._db.insert( + RSSTORRENTS( + TORRENT_NAME=title, + ENCLOSURE=enclosure + )) + + @DbPersist(_db) + def simple_delete_rss_torrents(self, title, enclosure): + """ + 删除RSS的记录 + """ + if enclosure: + self._db.query(RSSTORRENTS).filter(RSSTORRENTS.TORRENT_NAME == title, + RSSTORRENTS.ENCLOSURE == enclosure).delete() + else: + self._db.query(RSSTORRENTS).filter(RSSTORRENTS.TORRENT_NAME == title).delete() + + def is_douban_media_exists(self, media): + """ + 查询豆瓣是否存在 + """ + if not media: + return True + if self._db.query(DOUBANMEDIAS).filter(DOUBANMEDIAS.NAME == media.get_name()).count() > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_douban_media_state(self, media, state): + """ + 将豆瓣的数据插入数据库 + """ + if not media or not state: + return + if self.is_douban_media_exists(media): + return + else: + # 插入 + self._db.insert( + DOUBANMEDIAS( + NAME=media.get_name(), + YEAR=media.year, + TYPE=media.type.value, + RATING=media.vote_average, + IMAGE=media.get_poster_image(), + STATE=state, + ADD_TIME=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + ) + ) + + @DbPersist(_db) + def update_douban_media_state(self, media, state): + """ + 标记豆瓣数据的状态 + """ + self._db.query(DOUBANMEDIAS).filter(DOUBANMEDIAS.NAME == media.title, + DOUBANMEDIAS.YEAR == media.year).update( + { + "STATE": state + } + ) + + def get_douban_search_state(self, title, year=None): + """ + 查询未检索的豆瓣数据 + """ + if not year: + return self._db.query(DOUBANMEDIAS.STATE).filter(DOUBANMEDIAS.NAME == title).first() + else: + return self._db.query(DOUBANMEDIAS.STATE).filter(DOUBANMEDIAS.NAME == title, + DOUBANMEDIAS.YEAR == str(year)).first() + + def is_transfer_history_exists(self, source_path, source_filename, dest_path, dest_filename): + """ + 查询识别转移记录 + """ + if not source_path or not source_filename or not dest_path or not dest_filename: + return False + ret = self._db.query(TRANSFERHISTORY).filter(TRANSFERHISTORY.SOURCE_PATH == source_path, + TRANSFERHISTORY.SOURCE_FILENAME == source_filename, + TRANSFERHISTORY.DEST_PATH == dest_path, + TRANSFERHISTORY.DEST_FILENAME == dest_filename).count() + return True if ret > 0 else False + + @DbPersist(_db) + def insert_transfer_history(self, in_from: Enum, rmt_mode: RmtMode, in_path, out_path, dest, media_info): + """ + 插入识别转移记录 + """ + if not media_info or not media_info.tmdb_info: + return + if in_path: + in_path = os.path.normpath(in_path) + source_path = os.path.dirname(in_path) + source_filename = os.path.basename(in_path) + else: + return + if out_path: + outpath = os.path.normpath(out_path) + dest_path = os.path.dirname(outpath) + dest_filename = os.path.basename(outpath) + season_episode = media_info.get_season_episode_string() + else: + dest_path = "" + dest_filename = "" + season_episode = media_info.get_season_string() + title = media_info.title + if self.is_transfer_history_exists(source_path, source_filename, dest_path, dest_filename): + return + dest = dest or "" + timestr = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + self._db.insert( + TRANSFERHISTORY( + MODE=str(rmt_mode.value), + TYPE=media_info.type.value, + CATEGORY=media_info.category, + TMDBID=int(media_info.tmdb_id), + TITLE=title, + YEAR=media_info.year, + SEASON_EPISODE=season_episode, + SOURCE=str(in_from.value), + SOURCE_PATH=source_path, + SOURCE_FILENAME=source_filename, + DEST=dest, + DEST_PATH=dest_path, + DEST_FILENAME=dest_filename, + DATE=timestr + ) + ) + + def get_transfer_history(self, search, page, rownum): + """ + 查询识别转移记录 + """ + if int(page) == 1: + begin_pos = 0 + else: + begin_pos = (int(page) - 1) * int(rownum) + + if search: + search = f"%{search}%" + count = self._db.query(TRANSFERHISTORY).filter((TRANSFERHISTORY.SOURCE_FILENAME.like(search)) + | (TRANSFERHISTORY.TITLE.like(search))).count() + data = self._db.query(TRANSFERHISTORY).filter((TRANSFERHISTORY.SOURCE_FILENAME.like(search)) + | (TRANSFERHISTORY.TITLE.like(search))).order_by( + TRANSFERHISTORY.DATE.desc()).limit(int(rownum)).offset(begin_pos).all() + return count, data + else: + return self._db.query(TRANSFERHISTORY).count(), self._db.query(TRANSFERHISTORY).order_by( + TRANSFERHISTORY.DATE.desc()).limit(int(rownum)).offset(begin_pos).all() + + def get_transfer_path_by_id(self, logid): + """ + 据logid查询PATH + """ + return self._db.query(TRANSFERHISTORY).filter(TRANSFERHISTORY.ID == int(logid)).all() + + def is_transfer_history_exists_by_source_full_path(self, source_full_path): + """ + 据源文件的全路径查询识别转移记录 + """ + + path = os.path.dirname(source_full_path) + filename = os.path.basename(source_full_path) + ret = self._db.query(TRANSFERHISTORY).filter(TRANSFERHISTORY.SOURCE_PATH == path, + TRANSFERHISTORY.SOURCE_FILENAME == filename).count() + if ret > 0: + return True + else: + return False + + @DbPersist(_db) + def delete_transfer_log_by_id(self, logid): + """ + 根据logid删除记录 + """ + self._db.query(TRANSFERHISTORY).filter(TRANSFERHISTORY.ID == int(logid)).delete() + + def get_transfer_unknown_paths(self, ): + """ + 查询未识别的记录列表 + """ + return self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.STATE == 'N').all() + + @DbPersist(_db) + def update_transfer_unknown_state(self, path): + """ + 更新未识别记录为识别 + """ + if not path: + return + self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.PATH == os.path.normpath(path)).update( + { + "STATE": "Y" + } + ) + + @DbPersist(_db) + def delete_transfer_unknown(self, tid): + """ + 删除未识别记录 + """ + if not tid: + return [] + self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.ID == int(tid)).delete() + + def get_unknown_path_by_id(self, tid): + """ + 查询未识别记录 + """ + if not tid: + return [] + return self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.ID == int(tid)).all() + + def get_transfer_unknown_by_path(self, path): + """ + 根据路径查询未识别记录 + """ + if not path: + return [] + return self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.PATH == path).all() + + def is_transfer_unknown_exists(self, path): + """ + 查询未识别记录是否存在 + """ + if not path: + return False + ret = self._db.query(TRANSFERUNKNOWN).filter(TRANSFERUNKNOWN.PATH == os.path.normpath(path)).count() + if ret > 0: + return True + else: + return False + + def is_need_insert_transfer_unknown(self, path): + """ + 检查是否需要插入未识别记录 + """ + if not path: + return False + + """ + 1) 如果不存在未识别,则插入 + 2) 如果存在未处理的未识别,则插入(并不会真正的插入,insert_transfer_unknown里会挡住,主要是标记进行消息推送) + 3) 如果未识别已经全部处理完并且存在转移记录,则不插入 + 4) 如果未识别已经全部处理完并且不存在转移记录,则删除并重新插入 + """ + unknowns = self.get_transfer_unknown_by_path(path) + if unknowns: + is_all_proceed = True + for unknown in unknowns: + if unknown.STATE == 'N': + is_all_proceed = False + break + + if is_all_proceed: + is_transfer_history_exists = self.is_transfer_history_exists_by_source_full_path(path) + if is_transfer_history_exists: + # 对应 3) + return False + else: + # 对应 4) + for unknown in unknowns: + self.delete_transfer_unknown(unknown.ID) + return True + else: + # 对应 2) + return True + else: + # 对应 1) + return True + + @DbPersist(_db) + def insert_transfer_unknown(self, path, dest, rmt_mode): + """ + 插入未识别记录 + """ + if not path: + return + if self.is_transfer_unknown_exists(path): + return + else: + path = os.path.normpath(path) + if dest: + dest = os.path.normpath(dest) + else: + dest = "" + self._db.insert(TRANSFERUNKNOWN( + PATH=path, + DEST=dest, + STATE='N', + MODE=str(rmt_mode.value) + )) + + def is_transfer_in_blacklist(self, path): + """ + 查询是否为黑名单 + """ + if not path: + return False + ret = self._db.query(TRANSFERBLACKLIST).filter(TRANSFERBLACKLIST.PATH == os.path.normpath(path)).count() + if ret > 0: + return True + else: + return False + + def is_transfer_notin_blacklist(self, path): + """ + 查询是否为黑名单 + """ + return not self.is_transfer_in_blacklist(path) + + @DbPersist(_db) + def insert_transfer_blacklist(self, path): + """ + 插入黑名单记录 + """ + if not path: + return + if self.is_transfer_in_blacklist(path): + return + else: + self._db.insert(TRANSFERBLACKLIST( + PATH=os.path.normpath(path) + )) + + @DbPersist(_db) + def truncate_transfer_blacklist(self, ): + """ + 清空黑名单记录 + """ + self._db.query(TRANSFERBLACKLIST).delete() + self._db.query(SYNCHISTORY).delete() + + @DbPersist(_db) + def truncate_rss_history(self, ): + """ + 清空RSS历史记录 + """ + self._db.query(RSSTORRENTS).delete() + + @DbPersist(_db) + def truncate_rss_episodes(self, ): + """ + 清空RSS历史记录 + """ + self._db.query(RSSTVEPISODES).delete() + + def get_config_site(self, ): + """ + 查询所有站点信息 + """ + return self._db.query(CONFIGSITE).order_by(cast(CONFIGSITE.PRI, Integer).asc()) + + def get_site_by_id(self, tid): + """ + 查询1个站点信息 + """ + return self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).all() + + def get_site_by_name(self, name): + """ + 基于站点名称查询站点信息 + :return: + """ + return self._db.query(CONFIGSITE).filter(CONFIGSITE.NAME == name).all() + + @DbPersist(_db) + def insert_config_site(self, name, site_pri, rssurl, signurl, cookie, note, rss_uses): + """ + 插入站点信息 + """ + if not name: + return + self._db.insert(CONFIGSITE( + NAME=name, + PRI=site_pri, + RSSURL=rssurl, + SIGNURL=signurl, + COOKIE=cookie, + NOTE=note, + INCLUDE=rss_uses + )) + + @DbPersist(_db) + def delete_config_site(self, tid): + """ + 删除站点信息 + """ + if not tid: + return + self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).delete() + + @DbPersist(_db) + def update_config_site(self, tid, name, site_pri, rssurl, signurl, cookie, note, rss_uses): + """ + 更新站点信息 + """ + if not tid: + return + self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).update( + { + "NAME": name, + "PRI": site_pri, + "RSSURL": rssurl, + "SIGNURL": signurl, + "COOKIE": cookie, + "NOTE": note, + "INCLUDE": rss_uses + } + ) + + @DbPersist(_db) + def update_config_site_note(self, tid, note): + """ + 更新站点属性 + """ + if not tid: + return + self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).update( + { + "NOTE": note + } + ) + + @DbPersist(_db) + def update_site_cookie_ua(self, tid, cookie, ua=None): + """ + 更新站点Cookie和ua + """ + if not tid: + return + rec = self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).first() + if rec.NOTE: + note = json.loads(rec.NOTE) + if ua: + note['ua'] = ua + else: + note = {} + self._db.query(CONFIGSITE).filter(CONFIGSITE.ID == int(tid)).update( + { + "COOKIE": cookie, + "NOTE": json.dumps(note) + } + ) + + def get_config_filter_group(self, gid=None): + """ + 查询过滤规则组 + """ + if gid: + return self._db.query(CONFIGFILTERGROUP).filter(CONFIGFILTERGROUP.ID == int(gid)).all() + return self._db.query(CONFIGFILTERGROUP).all() + + def get_config_filter_rule(self, groupid=None): + """ + 查询过滤规则 + """ + if not groupid: + return self._db.query(CONFIGFILTERRULES).order_by(CONFIGFILTERRULES.GROUP_ID, + cast(CONFIGFILTERRULES.PRIORITY, + Integer)).all() + else: + return self._db.query(CONFIGFILTERRULES).filter( + CONFIGFILTERRULES.GROUP_ID == int(groupid)).order_by(CONFIGFILTERRULES.GROUP_ID, + cast(CONFIGFILTERRULES.PRIORITY, + Integer)).all() + + def get_rss_movies(self, state=None, rssid=None): + """ + 查询订阅电影信息 + """ + if rssid: + return self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rssid)).all() + else: + if not state: + return self._db.query(RSSMOVIES).all() + else: + return self._db.query(RSSMOVIES).filter(RSSMOVIES.STATE == state).all() + + def get_rss_movie_id(self, title, year=None, tmdbid=None): + """ + 获取订阅电影ID + """ + if not title: + return "" + if tmdbid: + ret = self._db.query(RSSMOVIES.ID).filter(RSSMOVIES.TMDBID == str(tmdbid)).first() + if ret: + return ret[0] + if not year: + items = self._db.query(RSSMOVIES).filter(RSSMOVIES.NAME == title).all() + else: + items = self._db.query(RSSMOVIES).filter(RSSMOVIES.NAME == title, + RSSMOVIES.YEAR == str(year)).all() + if items: + if tmdbid: + for item in items: + if not item.TMDBID or item.TMDBID == str(tmdbid): + return item.ID + else: + return items[0].ID + else: + return "" + + def get_rss_movie_sites(self, rssid): + """ + 获取订阅电影站点 + """ + if not rssid: + return "" + ret = self._db.query(RSSMOVIES.DESC).filter(RSSMOVIES.ID == int(rssid)).first() + if ret: + return ret[0] + return "" + + @DbPersist(_db) + def update_rss_movie_tmdb(self, rid, tmdbid, title, year, image, desc, note): + """ + 更新订阅电影的部分信息 + """ + if not tmdbid: + return + self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rid)).update({ + "TMDBID": tmdbid, + "NAME": title, + "YEAR": year, + "IMAGE": image, + "NOTE": note, + "DESC": desc + }) + + @DbPersist(_db) + def update_rss_movie_desc(self, rid, desc): + """ + 更新订阅电影的DESC + """ + self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rid)).update({ + "DESC": desc + }) + + @DbPersist(_db) + def update_rss_filter_order(self, rtype, rssid, res_order): + """ + 更新订阅命中的过滤规则优先级 + """ + if rtype == MediaType.MOVIE: + self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rssid)).update({ + "FILTER_ORDER": res_order + }) + else: + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).update({ + "FILTER_ORDER": res_order + }) + + def get_rss_overedition_order(self, rtype, rssid): + """ + 查询当前订阅的过滤优先级 + """ + if rtype == MediaType.MOVIE: + res = self._db.query(RSSMOVIES.FILTER_ORDER).filter(RSSMOVIES.ID == int(rssid)).first() + else: + res = self._db.query(RSSTVS.FILTER_ORDER).filter(RSSTVS.ID == int(rssid)).first() + if res and res[0]: + return int(res[0]) + else: + return 0 + + def is_exists_rss_movie(self, title, year): + """ + 判断RSS电影是否存在 + """ + if not title: + return False + count = self._db.query(RSSMOVIES).filter(RSSMOVIES.NAME == title, + RSSMOVIES.YEAR == str(year)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_rss_movie(self, media_info, + state='D', + rss_sites=None, + search_sites=None, + over_edition=0, + filter_restype=None, + filter_pix=None, + filter_team=None, + filter_rule=None, + save_path=None, + download_setting=-1, + fuzzy_match=0, + desc=None, + note=None, + keyword=None): + """ + 新增RSS电影 + """ + if search_sites is None: + search_sites = [] + if rss_sites is None: + rss_sites = [] + if not media_info: + return -1 + if not media_info.title: + return -1 + if self.is_exists_rss_movie(media_info.title, media_info.year): + return 9 + self._db.insert(RSSMOVIES( + NAME=media_info.title, + YEAR=media_info.year, + TMDBID=media_info.tmdb_id, + IMAGE=media_info.get_message_image(), + RSS_SITES=json.dumps(rss_sites), + SEARCH_SITES=json.dumps(search_sites), + OVER_EDITION=over_edition, + FILTER_RESTYPE=filter_restype, + FILTER_PIX=filter_pix, + FILTER_RULE=filter_rule, + FILTER_TEAM=filter_team, + SAVE_PATH=save_path, + DOWNLOAD_SETTING=download_setting, + FUZZY_MATCH=fuzzy_match, + STATE=state, + DESC=desc, + NOTE=note, + KEYWORD=keyword + )) + return 0 + + @DbPersist(_db) + def delete_rss_movie(self, title=None, year=None, rssid=None, tmdbid=None): + """ + 删除RSS电影 + """ + if not title and not rssid: + return + if rssid: + self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rssid)).delete() + else: + if tmdbid: + self._db.query(RSSMOVIES).filter(RSSMOVIES.TMDBID == tmdbid).delete() + self._db.query(RSSMOVIES).filter(RSSMOVIES.NAME == title, + RSSMOVIES.YEAR == str(year)).delete() + + @DbPersist(_db) + def update_rss_movie_state(self, title=None, year=None, rssid=None, state='R'): + """ + 更新电影订阅状态 + """ + if not title and not rssid: + return + if rssid: + self._db.query(RSSMOVIES).filter(RSSMOVIES.ID == int(rssid)).update( + { + "STATE": state + }) + else: + self._db.query(RSSMOVIES).filter(RSSMOVIES.NAME == title, + RSSMOVIES.YEAR == str(year)).update( + { + "STATE": state + }) + + def get_rss_tvs(self, state=None, rssid=None): + """ + 查询订阅电视剧信息 + """ + if rssid: + return self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).all() + else: + if not state: + return self._db.query(RSSTVS).all() + else: + return self._db.query(RSSTVS).filter(RSSTVS.STATE == state).all() + + def get_rss_tv_id(self, title, year=None, season=None, tmdbid=None): + """ + 获取订阅电视剧ID + """ + if not title: + return "" + if tmdbid: + if season: + ret = self._db.query(RSSTVS.ID).filter(RSSTVS.TMDBID == tmdbid, + RSSTVS.SEASON == season).first() + else: + ret = self._db.query(RSSTVS.ID).filter(RSSTVS.TMDBID == tmdbid).first() + if ret: + return ret[0] + if season and year: + items = self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.SEASON == str(season), + RSSTVS.YEAR == str(year)).all() + elif season and not year: + items = self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.SEASON == str(season)).all() + elif not season and year: + items = self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.YEAR == str(year)).all() + else: + items = self._db.query(RSSTVS).filter(RSSTVS.NAME == title).all() + if items: + if tmdbid: + for item in items: + if not item.TMDBID or item.TMDBID == str(tmdbid): + return item.ID + else: + return items[0].ID + else: + return "" + + def get_rss_tv_sites(self, rssid): + """ + 获取订阅电视剧站点 + """ + if not rssid: + return "" + ret = self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).first() + if ret: + return ret + return "" + + @DbPersist(_db) + def update_rss_tv_tmdb(self, rid, tmdbid, title, year, total, lack, image, desc, note): + """ + 更新订阅电影的TMDBID + """ + if not tmdbid: + return + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rid)).update( + { + "TMDBID": tmdbid, + "NAME": title, + "YEAR": year, + "TOTAL": total, + "LACK": lack, + "IMAGE": image, + "DESC": desc, + "NOTE": note + } + ) + + @DbPersist(_db) + def update_rss_tv_desc(self, rid, desc): + """ + 更新订阅电视剧的DESC + """ + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rid)).update( + { + "DESC": desc + } + ) + + def is_exists_rss_tv(self, title, year, season=None): + """ + 判断RSS电视剧是否存在 + """ + if not title: + return False + if season: + count = self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.YEAR == str(year), + RSSTVS.SEASON == season).count() + else: + count = self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.YEAR == str(year)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_rss_tv(self, + media_info, + total, + lack=0, + state="D", + rss_sites=None, + search_sites=None, + over_edition=0, + filter_restype=None, + filter_pix=None, + filter_team=None, + filter_rule=None, + save_path=None, + download_setting=-1, + total_ep=None, + current_ep=None, + fuzzy_match=0, + desc=None, + note=None, + keyword=None): + """ + 新增RSS电视剧 + """ + if search_sites is None: + search_sites = [] + if rss_sites is None: + rss_sites = [] + if not media_info: + return -1 + if not media_info.title: + return -1 + if fuzzy_match and media_info.begin_season is None: + season_str = "" + else: + season_str = media_info.get_season_string() + if self.is_exists_rss_tv(media_info.title, media_info.year, season_str): + return 9 + self._db.insert(RSSTVS( + NAME=media_info.title, + YEAR=media_info.year, + SEASON=season_str, + TMDBID=media_info.tmdb_id, + IMAGE=media_info.get_message_image(), + RSS_SITES=json.dumps(rss_sites), + SEARCH_SITES=json.dumps(search_sites), + OVER_EDITION=over_edition, + FILTER_RESTYPE=filter_restype, + FILTER_PIX=filter_pix, + FILTER_RULE=filter_rule, + FILTER_TEAM=filter_team, + SAVE_PATH=save_path, + DOWNLOAD_SETTING=download_setting, + FUZZY_MATCH=fuzzy_match, + TOTAL_EP=total_ep, + CURRENT_EP=current_ep, + TOTAL=total, + LACK=lack, + STATE=state, + DESC=desc, + NOTE=note, + KEYWORD=keyword + )) + return 0 + + @DbPersist(_db) + def update_rss_tv_lack(self, title=None, year=None, season=None, rssid=None, lack_episodes: list = None): + """ + 更新电视剧缺失的集数 + """ + if not title and not rssid: + return + if not lack_episodes: + lack = 0 + else: + lack = len(lack_episodes) + if rssid: + self.update_rss_tv_episodes(rssid, lack_episodes) + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).update( + { + "LACK": lack + } + ) + else: + self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.YEAR == str(year), + RSSTVS.SEASON == season).update( + { + "LACK": lack + } + ) + + @DbPersist(_db) + def delete_rss_tv(self, title=None, season=None, rssid=None, tmdbid=None): + """ + 删除RSS电视剧 + """ + if not title and not rssid: + return + if not rssid: + rssid = self.get_rss_tv_id(title=title, tmdbid=tmdbid, season=season) + if rssid: + self.delete_rss_tv_episodes(rssid) + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).delete() + + def is_exists_rss_tv_episodes(self, rid): + """ + 判断RSS电视剧是否存在 + """ + if not rid: + return False + count = self._db.query(RSSTVEPISODES).filter(RSSTVEPISODES.RSSID == int(rid)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def update_rss_tv_episodes(self, rid, episodes): + """ + 插入或更新电视剧订阅缺失剧集 + """ + if not rid: + return + if not episodes: + episodes = [] + else: + episodes = [str(epi) for epi in episodes] + if self.is_exists_rss_tv_episodes(rid): + self._db.query(RSSTVEPISODES).filter(RSSTVEPISODES.RSSID == int(rid)).update( + { + "EPISODES": ",".join(episodes) + } + ) + else: + self._db.insert(RSSTVEPISODES( + RSSID=rid, + EPISODES=",".join(episodes) + )) + + def get_rss_tv_episodes(self, rid): + """ + 查询电视剧订阅缺失剧集 + """ + if not rid: + return [] + ret = self._db.query(RSSTVEPISODES.EPISODES).filter(RSSTVEPISODES.RSSID == rid).first() + if ret: + return [int(epi) for epi in str(ret[0]).split(',')] + else: + return None + + @DbPersist(_db) + def delete_rss_tv_episodes(self, rid): + """ + 删除电视剧订阅缺失剧集 + """ + if not rid: + return + self._db.query(RSSTVEPISODES).filter(RSSTVEPISODES.RSSID == int(rid)).delete() + + @DbPersist(_db) + def update_rss_tv_state(self, title=None, year=None, season=None, rssid=None, state='R'): + """ + 更新电视剧订阅状态 + """ + if not title and not rssid: + return + if rssid: + self._db.query(RSSTVS).filter(RSSTVS.ID == int(rssid)).update( + { + "STATE": state + }) + else: + self._db.query(RSSTVS).filter(RSSTVS.NAME == title, + RSSTVS.YEAR == str(year), + RSSTVS.SEASON == season).update( + { + "STATE": state + }) + + def is_sync_in_history(self, path, dest): + """ + 查询是否存在同步历史记录 + """ + if not path: + return False + count = self._db.query(SYNCHISTORY).filter(SYNCHISTORY.PATH == os.path.normpath(path), + SYNCHISTORY.DEST == os.path.normpath(dest)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_sync_history(self, path, src, dest): + """ + 插入黑名单记录 + """ + if not path or not dest: + return + if self.is_sync_in_history(path, dest): + return + else: + self._db.insert(SYNCHISTORY( + PATH=os.path.normpath(path), + SRC=os.path.normpath(src), + DEST=os.path.normpath(dest) + )) + + def get_users(self, ): + """ + 查询用户列表 + """ + return self._db.query(CONFIGUSERS).all() + + def is_user_exists(self, name): + """ + 判断用户是否存在 + """ + if not name: + return False + count = self._db.query(CONFIGUSERS).filter(CONFIGUSERS.NAME == name).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_user(self, name, password, pris): + """ + 新增用户 + """ + if not name or not password: + return + if self.is_user_exists(name): + return + else: + self._db.insert(CONFIGUSERS( + NAME=name, + PASSWORD=password, + PRIS=pris + )) + + @DbPersist(_db) + def delete_user(self, name): + """ + 删除用户 + """ + self._db.query(CONFIGUSERS).filter(CONFIGUSERS.NAME == name).delete() + + def get_transfer_statistics(self, days=30): + """ + 查询历史记录统计 + """ + begin_date = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S") + return self._db.query(TRANSFERHISTORY.TYPE, + func.substr(TRANSFERHISTORY.DATE, 1, 10), + func.count('*') + ).filter(TRANSFERHISTORY.DATE > begin_date).group_by( + func.substr(TRANSFERHISTORY.DATE, 1, 10) + ).order_by(TRANSFERHISTORY.DATE).all() + + @DbPersist(_db) + def update_site_user_statistics_site_name(self, new_name, old_name): + """ + 更新站点用户数据中站点名称 + """ + self._db.query(SITEUSERINFOSTATS).filter(SITEUSERINFOSTATS.SITE == old_name).update( + { + "SITE": new_name + } + ) + + @DbPersist(_db) + def update_site_user_statistics(self, site_user_infos: list): + """ + 更新站点用户粒度数据 + """ + if not site_user_infos: + return + update_at = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + for site_user_info in site_user_infos: + site = site_user_info.site_name + username = site_user_info.username + user_level = site_user_info.user_level + join_at = site_user_info.join_at + upload = site_user_info.upload + download = site_user_info.download + ratio = site_user_info.ratio + seeding = site_user_info.seeding + seeding_size = site_user_info.seeding_size + leeching = site_user_info.leeching + bonus = site_user_info.bonus + url = site_user_info.site_url + msg_unread = site_user_info.message_unread + if not self.is_exists_site_user_statistics(url): + self._db.insert(SITEUSERINFOSTATS( + SITE=site, + USERNAME=username, + USER_LEVEL=user_level, + JOIN_AT=join_at, + UPDATE_AT=update_at, + UPLOAD=upload, + DOWNLOAD=download, + RATIO=ratio, + SEEDING=seeding, + LEECHING=leeching, + SEEDING_SIZE=seeding_size, + BONUS=bonus, + URL=url, + MSG_UNREAD=msg_unread + )) + else: + self._db.query(SITEUSERINFOSTATS).filter(SITEUSERINFOSTATS.URL == url).update( + { + "SITE": site, + "USERNAME": username, + "USER_LEVEL": user_level, + "JOIN_AT": join_at, + "UPDATE_AT": update_at, + "UPLOAD": upload, + "DOWNLOAD": download, + "RATIO": ratio, + "SEEDING": seeding, + "LEECHING": leeching, + "SEEDING_SIZE": seeding_size, + "BONUS": bonus, + "MSG_UNREAD": msg_unread + } + ) + + def is_exists_site_user_statistics(self, url): + """ + 判断站点数据是滞存在 + """ + count = self._db.query(SITEUSERINFOSTATS).filter(SITEUSERINFOSTATS.URL == url).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def update_site_favicon(self, site_user_infos: list): + """ + 更新站点图标数据 + """ + if not site_user_infos: + return + for site_user_info in site_user_infos: + site_icon = "data:image/ico;base64," + \ + site_user_info.site_favicon if site_user_info.site_favicon else site_user_info.site_url \ + + "/favicon.ico" + if not self.is_exists_site_favicon(site_user_info.site_name): + self._db.insert(SITEFAVICON( + SITE=site_user_info.site_name, + URL=site_user_info.site_url, + FAVICON=site_icon + )) + elif site_user_info.site_favicon: + self._db.query(SITEFAVICON).filter(SITEFAVICON.SITE == site_user_info.site_name).update( + { + "URL": site_user_info.site_url, + "FAVICON": site_icon + } + ) + + def is_exists_site_favicon(self, site): + """ + 判断站点图标是否存在 + """ + count = self._db.query(SITEFAVICON).filter(SITEFAVICON.SITE == site).count() + if count > 0: + return True + else: + return False + + def get_site_favicons(self, site=None): + """ + 查询站点数据历史 + """ + if site: + return self._db.query(SITEFAVICON).filter(SITEFAVICON.SITE == site).all() + else: + return self._db.query(SITEFAVICON).all() + + @DbPersist(_db) + def update_site_seed_info_site_name(self, new_name, old_name): + """ + 更新站点做种数据中站点名称 + :param new_name: 新的站点名称 + :param old_name: 原始站点名称 + :return: + """ + self._db.query(SITEUSERSEEDINGINFO).filter(SITEUSERSEEDINGINFO.SITE == old_name).update( + { + "SITE": new_name + } + ) + + @DbPersist(_db) + def update_site_seed_info(self, site_user_infos: list): + """ + 更新站点做种数据 + """ + if not site_user_infos: + return + update_at = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + for site_user_info in site_user_infos: + if not self.is_site_seeding_info_exist(url=site_user_info.site_url): + self._db.insert(SITEUSERSEEDINGINFO( + SITE=site_user_info.site_name, + UPDATE_AT=update_at, + SEEDING_INFO=site_user_info.seeding_info, + URL=site_user_info.site_url + )) + else: + self._db.query(SITEUSERSEEDINGINFO).filter(SITEUSERSEEDINGINFO.URL == site_user_info.site_url).update( + { + "SITE": site_user_info.site_name, + "UPDATE_AT": update_at, + "SEEDING_INFO": site_user_info.seeding_info + } + ) + + def is_site_user_statistics_exists(self, url): + """ + 判断站点用户数据是否存在 + """ + if not url: + return False + count = self._db.query(SITEUSERINFOSTATS).filter(SITEUSERINFOSTATS.URL == url).count() + if count > 0: + return True + else: + return False + + def get_site_user_statistics(self, num=100, strict_urls=None): + """ + 查询站点数据历史 + """ + if strict_urls: + # 根据站点优先级排序 + return self._db.query(SITEUSERINFOSTATS) \ + .join(CONFIGSITE, SITEUSERINFOSTATS.SITE == CONFIGSITE.NAME) \ + .filter(SITEUSERINFOSTATS.URL.in_(tuple(strict_urls + ["__DUMMY__"]))) \ + .order_by(cast(CONFIGSITE.PRI, Integer).asc()).limit(num).all() + else: + return self._db.query(SITEUSERINFOSTATS).limit(num).all() + + def is_site_statistics_history_exists(self, url, date): + """ + 判断站点历史数据是否存在 + """ + if not url or not date: + return False + count = self._db.query(SITESTATISTICSHISTORY).filter(SITESTATISTICSHISTORY.URL == url, + SITESTATISTICSHISTORY.DATE == date).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def update_site_statistics_site_name(self, new_name, old_name): + """ + 更新站点做种数据中站点名称 + :param new_name: 新站点名称 + :param old_name: 原始站点名称 + :return: + """ + self._db.query(SITESTATISTICSHISTORY).filter(SITESTATISTICSHISTORY.SITE == old_name).update( + { + "SITE": new_name + } + ) + + @DbPersist(_db) + def insert_site_statistics_history(self, site_user_infos: list): + """ + 插入站点数据 + """ + if not site_user_infos: + return + date_now = time.strftime('%Y-%m-%d', time.localtime(time.time())) + for site_user_info in site_user_infos: + site = site_user_info.site_name + upload = site_user_info.upload + user_level = site_user_info.user_level + download = site_user_info.download + ratio = site_user_info.ratio + seeding = site_user_info.seeding + seeding_size = site_user_info.seeding_size + leeching = site_user_info.leeching + bonus = site_user_info.bonus + url = site_user_info.site_url + if not self.is_site_statistics_history_exists(date=date_now, url=url): + self._db.insert(SITESTATISTICSHISTORY( + SITE=site, + USER_LEVEL=user_level, + DATE=date_now, + UPLOAD=upload, + DOWNLOAD=download, + RATIO=ratio, + SEEDING=seeding, + LEECHING=leeching, + SEEDING_SIZE=seeding_size, + BONUS=bonus, + URL=url + )) + else: + self._db.query(SITESTATISTICSHISTORY).filter(SITESTATISTICSHISTORY.DATE == date_now, + SITESTATISTICSHISTORY.URL == url).update( + { + "SITE": site, + "USER_LEVEL": user_level, + "UPLOAD": upload, + "DOWNLOAD": download, + "RATIO": ratio, + "SEEDING": seeding, + "LEECHING": leeching, + "SEEDING_SIZE": seeding_size, + "BONUS": bonus + } + ) + + def get_site_statistics_history(self, site, days=30): + """ + 查询站点数据历史 + """ + return self._db.query(SITESTATISTICSHISTORY).filter( + SITESTATISTICSHISTORY.SITE == site).order_by( + SITESTATISTICSHISTORY.DATE.asc() + ).limit(days) + + def get_site_seeding_info(self, site): + """ + 查询站点做种信息 + """ + return self._db.query(SITEUSERSEEDINGINFO.SEEDING_INFO).filter( + SITEUSERSEEDINGINFO.SITE == site).first() + + def is_site_seeding_info_exist(self, url): + """ + 判断做种数据是否已存在 + """ + count = self._db.query(SITEUSERSEEDINGINFO).filter( + SITEUSERSEEDINGINFO.URL == url).count() + if count > 0: + return True + else: + return False + + def get_site_statistics_recent_sites(self, days=7, strict_urls=None): + """ + 查询近期上传下载量 + """ + # 查询最大最小日期 + if strict_urls is None: + strict_urls = [] + + b_date = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime("%Y-%m-%d") + date_ret = self._db.query(func.max(SITESTATISTICSHISTORY.DATE), + func.MIN(SITESTATISTICSHISTORY.DATE)).filter( + SITESTATISTICSHISTORY.DATE > b_date).all() + if date_ret and date_ret[0][0]: + total_upload = 0 + total_download = 0 + ret_site_uploads = [] + ret_site_downloads = [] + min_date = date_ret[0][1] + # 查询开始值 + if strict_urls: + subquery = self._db.query(SITESTATISTICSHISTORY.SITE.label("SITE"), + SITESTATISTICSHISTORY.DATE.label("DATE"), + func.sum(SITESTATISTICSHISTORY.UPLOAD).label("UPLOAD"), + func.sum(SITESTATISTICSHISTORY.DOWNLOAD).label("DOWNLOAD")).filter( + SITESTATISTICSHISTORY.DATE >= min_date, + SITESTATISTICSHISTORY.URL.in_(tuple(strict_urls + ["__DUMMY__"]))).group_by( + SITESTATISTICSHISTORY.SITE, SITESTATISTICSHISTORY.DATE).subquery() + else: + subquery = self._db.query(SITESTATISTICSHISTORY.SITE.label("SITE"), + SITESTATISTICSHISTORY.DATE.label("DATE"), + func.sum(SITESTATISTICSHISTORY.UPLOAD).label("UPLOAD"), + func.sum(SITESTATISTICSHISTORY.DOWNLOAD).label("DOWNLOAD")).filter( + SITESTATISTICSHISTORY.DATE >= min_date).group_by( + SITESTATISTICSHISTORY.SITE, SITESTATISTICSHISTORY.DATE).subquery() + rets = self._db.query(subquery.c.SITE, + func.min(subquery.c.UPLOAD), + func.min(subquery.c.DOWNLOAD), + func.max(subquery.c.UPLOAD), + func.max(subquery.c.DOWNLOAD)).group_by(subquery.c.SITE).all() + ret_sites = [] + for ret_b in rets: + # 如果最小值都是0,可能时由于近几日没有更新数据,或者cookie过期,正常有数据的话,第二天能正常 + ret_b = list(ret_b) + if ret_b[1] == 0 and ret_b[2] == 0: + ret_b[1] = ret_b[3] + ret_b[2] = ret_b[4] + ret_sites.append(ret_b[0]) + if int(ret_b[1]) < int(ret_b[3]): + total_upload += int(ret_b[3]) - int(ret_b[1]) + ret_site_uploads.append(int(ret_b[3]) - int(ret_b[1])) + else: + ret_site_uploads.append(0) + if int(ret_b[2]) < int(ret_b[4]): + total_download += int(ret_b[4]) - int(ret_b[2]) + ret_site_downloads.append(int(ret_b[4]) - int(ret_b[2])) + else: + ret_site_downloads.append(0) + return total_upload, total_download, ret_sites, ret_site_uploads, ret_site_downloads + else: + return 0, 0, [], [], [] + + def is_exists_download_history(self, title, tmdbid, mtype=None): + """ + 查询下载历史是否存在 + """ + if not title or not tmdbid: + return False + if mtype: + count = self._db.query(DOWNLOADHISTORY).filter( + (DOWNLOADHISTORY.TITLE == title) | (DOWNLOADHISTORY.TMDBID == tmdbid), + DOWNLOADHISTORY.TYPE == mtype).count() + else: + count = self._db.query(DOWNLOADHISTORY).filter( + (DOWNLOADHISTORY.TITLE == title) | (DOWNLOADHISTORY.TMDBID == tmdbid)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_download_history(self, media_info): + """ + 新增下载历史 + """ + if not media_info: + return + if not media_info.title or not media_info.tmdb_id: + return + if self.is_exists_download_history(media_info.title, media_info.tmdb_id, media_info.type.value): + self._db.query(DOWNLOADHISTORY).filter(DOWNLOADHISTORY.TITLE == media_info.title, + DOWNLOADHISTORY.TMDBID == media_info.tmdb_id, + DOWNLOADHISTORY.TYPE == media_info.type.value).update( + { + "TORRENT": media_info.org_string, + "ENCLOSURE": media_info.enclosure, + "DESC": media_info.description, + "DATE": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + "SITE": media_info.site + } + ) + else: + self._db.insert(DOWNLOADHISTORY( + TITLE=media_info.title, + YEAR=media_info.year, + TYPE=media_info.type.value, + TMDBID=media_info.tmdb_id, + VOTE=media_info.vote_average, + POSTER=media_info.get_poster_image(), + OVERVIEW=media_info.overview, + TORRENT=media_info.org_string, + ENCLOSURE=media_info.enclosure, + DESC=media_info.description, + DATE=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + SITE=media_info.site + )) + + def get_download_history(self, date=None, hid=None, num=30, page=1): + """ + 查询下载历史 + """ + if hid: + return self._db.query(DOWNLOADHISTORY).filter(DOWNLOADHISTORY.ID == int(hid)).all() + elif date: + return self._db.query(DOWNLOADHISTORY).filter( + DOWNLOADHISTORY.DATE > date).order_by(DOWNLOADHISTORY.DATE.desc()).all() + else: + offset = (int(page) - 1) * int(num) + return self._db.query(DOWNLOADHISTORY).order_by( + DOWNLOADHISTORY.DATE.desc()).limit(num).offset(offset).all() + + def is_media_downloaded(self, title, tmdbid): + """ + 根据标题和年份检查是否下载过 + """ + if self.is_exists_download_history(title, tmdbid): + return True + count = self._db.query(TRANSFERHISTORY).filter(TRANSFERHISTORY.TITLE == title).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_brushtask(self, brush_id, item): + """ + 新增刷流任务 + """ + if not brush_id: + self._db.insert(SITEBRUSHTASK( + NAME=item.get('name'), + SITE=item.get('site'), + FREELEECH=item.get('free'), + RSS_RULE=str(item.get('rss_rule')), + REMOVE_RULE=str(item.get('remove_rule')), + SEED_SIZE=item.get('seed_size'), + INTEVAL=item.get('interval'), + DOWNLOADER=item.get('downloader'), + TRANSFER=item.get('transfer'), + DOWNLOAD_COUNT='0', + REMOVE_COUNT='0', + DOWNLOAD_SIZE='0', + UPLOAD_SIZE='0', + STATE=item.get('state'), + LST_MOD_DATE=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + SENDMESSAGE=item.get('sendmessage'), + FORCEUPLOAD=item.get('forceupload') + )) + else: + self._db.query(SITEBRUSHTASK).filter(SITEBRUSHTASK.ID == int(brush_id)).update( + { + "NAME": item.get('name'), + "SITE": item.get('site'), + "FREELEECH": item.get('free'), + "RSS_RULE": str(item.get('rss_rule')), + "REMOVE_RULE": str(item.get('remove_rule')), + "SEED_SIZE": item.get('seed_size'), + "INTEVAL": item.get('interval'), + "DOWNLOADER": item.get('downloader'), + "TRANSFER": item.get('transfer'), + "STATE": item.get('state'), + "LST_MOD_DATE": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + "SENDMESSAGE": item.get('sendmessage'), + "FORCEUPLOAD": item.get('forceupload') + } + ) + + @DbPersist(_db) + def delete_brushtask(self, brush_id): + """ + 删除刷流任务 + """ + self._db.query(SITEBRUSHTASK).filter(SITEBRUSHTASK.ID == int(brush_id)).delete() + self._db.query(SITEBRUSHTORRENTS).filter(SITEBRUSHTORRENTS.TASK_ID == brush_id).delete() + + def get_brushtasks(self, brush_id=None): + """ + 查询刷流任务 + """ + if brush_id: + return self._db.query(SITEBRUSHTASK).filter(SITEBRUSHTASK.ID == int(brush_id)).first() + else: + # 根据站点优先级排序 + return self._db.query(SITEBRUSHTASK) \ + .join(CONFIGSITE, SITEBRUSHTASK.SITE == CONFIGSITE.ID) \ + .order_by(cast(CONFIGSITE.PRI, Integer).asc()).all() + + def get_brushtask_totalsize(self, brush_id): + """ + 查询刷流任务总体积 + """ + if not brush_id: + return 0 + ret = self._db.query(func.sum(cast(SITEBRUSHTORRENTS.TORRENT_SIZE, + Integer))).filter(SITEBRUSHTORRENTS.TASK_ID == brush_id, + SITEBRUSHTORRENTS.DOWNLOAD_ID != '0').first() + if ret: + return ret[0] or 0 + else: + return 0 + + @DbPersist(_db) + def add_brushtask_download_count(self, brush_id): + """ + 增加刷流下载数 + """ + if not brush_id: + return + self._db.query(SITEBRUSHTASK).filter(SITEBRUSHTASK.ID == int(brush_id)).update( + { + "DOWNLOAD_COUNT": SITEBRUSHTASK.DOWNLOAD_COUNT + 1, + "LST_MOD_DATE": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + } + ) + + def get_brushtask_remove_size(self, brush_id): + """ + 获取已删除种子的上传量 + """ + if not brush_id: + return 0 + return self._db.query(SITEBRUSHTORRENTS.TORRENT_SIZE).filter(SITEBRUSHTORRENTS.TASK_ID == brush_id, + SITEBRUSHTORRENTS.DOWNLOAD_ID == '0').all() + + @DbPersist(_db) + def add_brushtask_upload_count(self, brush_id, upload_size, download_size, remove_count): + """ + 更新上传下载量和删除种子数 + """ + if not brush_id: + return + delete_upsize = 0 + delete_dlsize = 0 + remove_sizes = self.get_brushtask_remove_size(brush_id) + for remove_size in remove_sizes: + if not remove_size[0]: + continue + if str(remove_size[0]).find(",") != -1: + sizes = str(remove_size[0]).split(",") + delete_upsize += int(sizes[0] or 0) + if len(sizes) > 1: + delete_dlsize += int(sizes[1] or 0) + else: + delete_upsize += int(remove_size[0]) + self._db.query(SITEBRUSHTASK).filter(SITEBRUSHTASK.ID == int(brush_id)).update({ + "REMOVE_COUNT": SITEBRUSHTASK.REMOVE_COUNT + remove_count, + "UPLOAD_SIZE": int(upload_size) + delete_upsize, + "DOWNLOAD_SIZE": int(download_size) + delete_dlsize, + }) + + @DbPersist(_db) + def insert_brushtask_torrent(self, brush_id, title, enclosure, downloader, download_id, size): + """ + 增加刷流下载的种子信息 + """ + if not brush_id: + return + if self.is_brushtask_torrent_exists(brush_id, title, enclosure): + return + self._db.insert(SITEBRUSHTORRENTS( + TASK_ID=brush_id, + TORRENT_NAME=title, + TORRENT_SIZE=size, + ENCLOSURE=enclosure, + DOWNLOADER=downloader, + DOWNLOAD_ID=download_id, + LST_MOD_DATE=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + )) + + def get_brushtask_torrents(self, brush_id, active=True): + """ + 查询刷流任务所有种子 + """ + if not brush_id: + return [] + if active: + return self._db.query(SITEBRUSHTORRENTS).filter( + SITEBRUSHTORRENTS.TASK_ID == int(brush_id), + SITEBRUSHTORRENTS.DOWNLOAD_ID != '0').all() + else: + return self._db.query(SITEBRUSHTORRENTS).filter( + SITEBRUSHTORRENTS.TASK_ID == int(brush_id) + ).order_by(SITEBRUSHTORRENTS.LST_MOD_DATE.desc()).all() + + def is_brushtask_torrent_exists(self, brush_id, title, enclosure): + """ + 查询刷流任务种子是否已存在 + """ + if not brush_id: + return False + count = self._db.query(SITEBRUSHTORRENTS).filter(SITEBRUSHTORRENTS.TASK_ID == brush_id, + SITEBRUSHTORRENTS.TORRENT_NAME == title, + SITEBRUSHTORRENTS.ENCLOSURE == enclosure).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def update_brushtask_torrent_state(self, ids: list): + """ + 更新刷流种子的状态 + """ + if not ids: + return + for _id in ids: + self._db.query(SITEBRUSHTORRENTS).filter(SITEBRUSHTORRENTS.TASK_ID == _id[1], + SITEBRUSHTORRENTS.DOWNLOAD_ID == _id[2]).update( + { + "TORRENT_SIZE": _id[0], + "DOWNLOAD_ID": '0' + } + ) + + @DbPersist(_db) + def delete_brushtask_torrent(self, brush_id, download_id): + """ + 删除刷流种子记录 + """ + if not download_id or not brush_id: + return + self._db.query(SITEBRUSHTORRENTS).filter(SITEBRUSHTORRENTS.TASK_ID == brush_id, + SITEBRUSHTORRENTS.DOWNLOAD_ID == download_id).delete() + + def get_user_downloaders(self, did=None): + """ + 查询自定义下载器 + """ + if did: + return self._db.query(SITEBRUSHDOWNLOADERS).filter(SITEBRUSHDOWNLOADERS.ID == int(did)).first() + else: + return self._db.query(SITEBRUSHDOWNLOADERS).all() + + @DbPersist(_db) + def update_user_downloader(self, did, name, dtype, user_config, note): + """ + 新增自定义下载器 + """ + if did: + self._db.query(SITEBRUSHDOWNLOADERS).filter(SITEBRUSHDOWNLOADERS.ID == int(did)).update( + { + "NAME": name, + "TYPE": dtype, + "HOST": user_config.get("host"), + "PORT": user_config.get("port"), + "USERNAME": user_config.get("username"), + "PASSWORD": user_config.get("password"), + "SAVE_DIR": user_config.get("save_dir"), + "NOTE": note + } + ) + else: + self._db.insert(SITEBRUSHDOWNLOADERS( + NAME=name, + TYPE=dtype, + HOST=user_config.get("host"), + PORT=user_config.get("port"), + USERNAME=user_config.get("username"), + PASSWORD=user_config.get("password"), + SAVE_DIR=user_config.get("save_dir"), + NOTE=note + )) + + @DbPersist(_db) + def delete_user_downloader(self, did): + """ + 删除自定义下载器 + """ + self._db.query(SITEBRUSHDOWNLOADERS).filter(SITEBRUSHDOWNLOADERS.ID == int(did)).delete() + + @DbPersist(_db) + def add_filter_group(self, name, default='N'): + """ + 新增规则组 + """ + if default == 'Y': + self.set_default_filtergroup(0) + group_id = self.get_filter_groupid_by_name(name) + if group_id: + self._db.query(CONFIGFILTERGROUP).filter(CONFIGFILTERGROUP.ID == int(group_id)).update({ + "IS_DEFAULT": default + }) + else: + self._db.insert(CONFIGFILTERGROUP( + GROUP_NAME=name, + IS_DEFAULT=default + )) + + def get_filter_groupid_by_name(self, name): + ret = self._db.query(CONFIGFILTERGROUP.ID).filter(CONFIGFILTERGROUP.GROUP_NAME == name).first() + if ret: + return ret[0] + else: + return "" + + @DbPersist(_db) + def set_default_filtergroup(self, groupid): + """ + 设置默认的规则组 + """ + self._db.query(CONFIGFILTERGROUP).filter(CONFIGFILTERGROUP.ID == int(groupid)).update({ + "IS_DEFAULT": 'Y' + }) + self._db.query(CONFIGFILTERGROUP).filter(CONFIGFILTERGROUP.ID != int(groupid)).update({ + "IS_DEFAULT": 'N' + }) + + @DbPersist(_db) + def delete_filtergroup(self, groupid): + """ + 删除规则组 + """ + self._db.query(CONFIGFILTERRULES).filter(CONFIGFILTERRULES.GROUP_ID == groupid).delete() + self._db.query(CONFIGFILTERGROUP).filter(CONFIGFILTERGROUP.ID == int(groupid)).delete() + + @DbPersist(_db) + def delete_filterrule(self, ruleid): + """ + 删除规则 + """ + self._db.query(CONFIGFILTERRULES).filter(CONFIGFILTERRULES.ID == int(ruleid)).delete() + + @DbPersist(_db) + def insert_filter_rule(self, item, ruleid=None): + """ + 新增规则 + """ + if ruleid: + self._db.query(CONFIGFILTERRULES).filter(CONFIGFILTERRULES.ID == int(ruleid)).update( + { + "ROLE_NAME": item.get("name"), + "PRIORITY": item.get("pri"), + "INCLUDE": item.get("include"), + "EXCLUDE": item.get("exclude"), + "SIZE_LIMIT": item.get("size"), + "NOTE": item.get("free") + } + ) + else: + self._db.insert(CONFIGFILTERRULES( + GROUP_ID=item.get("group"), + ROLE_NAME=item.get("name"), + PRIORITY=item.get("pri"), + INCLUDE=item.get("include"), + EXCLUDE=item.get("exclude"), + SIZE_LIMIT=item.get("size"), + NOTE=item.get("free") + )) + + def get_userrss_tasks(self, tid=None): + if tid: + return self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(tid)).all() + else: + return self._db.query(CONFIGUSERRSS).order_by(CONFIGUSERRSS.STATE.desc()).all() + + @DbPersist(_db) + def delete_userrss_task(self, tid): + if not tid: + return + self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(tid)).delete() + + @DbPersist(_db) + def update_userrss_task_info(self, tid, count): + if not tid: + return + self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(tid)).update( + { + "PROCESS_COUNT": CONFIGUSERRSS.PROCESS_COUNT + count, + "UPDATE_TIME": time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(time.time())) + } + ) + + @DbPersist(_db) + def update_userrss_task(self, item): + if item.get("id") and self.get_userrss_tasks(item.get("id")): + self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(item.get("id"))).update( + { + "NAME": item.get("name"), + "ADDRESS": item.get("address"), + "PARSER": item.get("parser"), + "INTERVAL": item.get("interval"), + "USES": item.get("uses"), + "INCLUDE": item.get("include"), + "EXCLUDE": item.get("exclude"), + "FILTER": item.get("filter_rule"), + "UPDATE_TIME": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + "STATE": item.get("state"), + "SAVE_PATH": item.get("save_path"), + "DOWNLOAD_SETTING": item.get("download_setting"), + "RECOGNIZATION": item.get("recognization"), + "OVER_EDITION": int(item.get("over_edition")) if str(item.get("over_edition")).isdigit() else 0, + "SITES": json.dumps(item.get("sites")), + "FILTER_ARGS": json.dumps(item.get("filter_args")), + "NOTE": "" + } + ) + else: + self._db.insert(CONFIGUSERRSS( + NAME=item.get("name"), + ADDRESS=item.get("address"), + PARSER=item.get("parser"), + INTERVAL=item.get("interval"), + USES=item.get("uses"), + INCLUDE=item.get("include"), + EXCLUDE=item.get("exclude"), + FILTER=item.get("filter_rule"), + UPDATE_TIME=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), + STATE=item.get("state"), + SAVE_PATH=item.get("save_path"), + DOWNLOAD_SETTING=item.get("download_setting"), + RECOGNIZATION=item.get("recognization"), + OVER_EDITION=item.get("over_edition"), + SITES=json.dumps(item.get("sites")), + FILTER_ARGS=json.dumps(item.get("filter_args")), + PROCESS_COUNT='0' + )) + + @DbPersist(_db) + def insert_userrss_mediainfos(self, tid=None, mediainfo=None): + if not tid or not mediainfo: + return + taskinfo = self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(tid)).all() + if not taskinfo: + return + mediainfos = json.loads(taskinfo[0].MEDIAINFOS) if taskinfo[0].MEDIAINFOS else [] + tmdbid = str(mediainfo.tmdb_id) + season = int(mediainfo.get_season_seq()) + for media in mediainfos: + if media.get("id") == tmdbid and media.get("season") == season: + return + mediainfos.append({ + "id": tmdbid, + "rssid": "", + "season": season, + "name": mediainfo.title + }) + self._db.query(CONFIGUSERRSS).filter(CONFIGUSERRSS.ID == int(tid)).update( + { + "MEDIAINFOS": json.dumps(mediainfos) + }) + + def get_userrss_parser(self, pid=None): + if pid: + return self._db.query(CONFIGRSSPARSER).filter(CONFIGRSSPARSER.ID == int(pid)).first() + else: + return self._db.query(CONFIGRSSPARSER).all() + + @DbPersist(_db) + def delete_userrss_parser(self, pid): + if not pid: + return + self._db.query(CONFIGRSSPARSER).filter(CONFIGRSSPARSER.ID == int(pid)).delete() + + @DbPersist(_db) + def update_userrss_parser(self, item): + if not item: + return + if item.get("id") and self.get_userrss_parser(item.get("id")): + self._db.query(CONFIGRSSPARSER).filter(CONFIGRSSPARSER.ID == int(item.get("id"))).update( + { + "NAME": item.get("name"), + "TYPE": item.get("type"), + "FORMAT": item.get("format"), + "PARAMS": item.get("params") + } + ) + else: + self._db.insert(CONFIGRSSPARSER( + NAME=item.get("name"), + TYPE=item.get("type"), + FORMAT=item.get("format"), + PARAMS=item.get("params") + )) + + @DbPersist(_db) + def excute(self, sql): + return self._db.excute(sql) + + @DbPersist(_db) + def drop_table(self, table_name): + return self._db.excute(f"""DROP TABLE IF EXISTS {table_name}""") + + @DbPersist(_db) + def insert_userrss_task_history(self, task_id, title, downloader): + """ + 增加自定义RSS订阅任务的下载记录 + """ + self._db.insert(USERRSSTASKHISTORY( + TASK_ID=task_id, + TITLE=title, + DOWNLOADER=downloader, + DATE=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + )) + + def get_userrss_task_history(self, task_id): + """ + 查询自定义RSS订阅任务的下载记录 + """ + if not task_id: + return [] + return self._db.query(USERRSSTASKHISTORY).filter(USERRSSTASKHISTORY.TASK_ID == task_id) \ + .order_by(USERRSSTASKHISTORY.DATE.desc()).all() + + def get_rss_history(self, rtype=None, rid=None): + """ + 查询RSS历史 + """ + if rid: + return self._db.query(RSSHISTORY).filter(RSSHISTORY.ID == int(rid)).all() + elif rtype: + return self._db.query(RSSHISTORY).filter(RSSHISTORY.TYPE == rtype) \ + .order_by(RSSHISTORY.FINISH_TIME.desc()).all() + return self._db.query(RSSHISTORY).order_by(RSSHISTORY.FINISH_TIME.desc()).all() + + def is_exists_rss_history(self, rssid): + """ + 判断RSS历史是否存在 + """ + if not rssid: + return False + count = self._db.query(RSSHISTORY).filter(RSSHISTORY.RSSID == rssid).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_rss_history(self, rssid, rtype, name, year, tmdbid, image, desc, season=None, total=None, start=None): + """ + 登记RSS历史 + """ + if not self.is_exists_rss_history(rssid): + self._db.insert(RSSHISTORY( + TYPE=rtype, + RSSID=rssid, + NAME=name, + YEAR=year, + TMDBID=tmdbid, + SEASON=season, + IMAGE=image, + DESC=desc, + TOTAL=total, + START=start, + FINISH_TIME=time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(time.time())) + )) + + @DbPersist(_db) + def delete_rss_history(self, rssid): + """ + 删除RSS历史 + """ + if not rssid: + return + self._db.query(RSSHISTORY).filter(RSSHISTORY.ID == int(rssid)).delete() + + @DbPersist(_db) + def insert_custom_word(self, replaced, replace, front, back, offset, wtype, gid, season, enabled, regex, whelp, + note=None): + """ + 增加自定义识别词 + """ + self._db.insert(CUSTOMWORDS( + REPLACED=replaced, + REPLACE=replace, + FRONT=front, + BACK=back, + OFFSET=offset, + TYPE=int(wtype), + GROUP_ID=int(gid), + SEASON=int(season), + ENABLED=int(enabled), + REGEX=int(regex), + HELP=whelp, + NOTE=note + )) + + @DbPersist(_db) + def delete_custom_word(self, wid): + """ + 删除自定义识别词 + """ + self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.ID == int(wid)).delete() + + @DbPersist(_db) + def check_custom_word(self, wid, enabled): + """ + 设置自定义识别词状态 + """ + self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.ID == int(wid)).update( + { + "ENABLED": int(enabled) + } + ) + + def get_custom_words(self, wid=None, gid=None, enabled=None, wtype=None, regex=None): + """ + 查询自定义识别词 + """ + if wid: + return self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.ID == int(wid)) \ + .order_by(CUSTOMWORDS.GROUP_ID).all() + elif gid: + return self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.GROUP_ID == int(gid)) \ + .order_by(CUSTOMWORDS.GROUP_ID).all() + elif wtype and enabled is not None and regex is not None: + return self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.ENABLED == int(enabled), + CUSTOMWORDS.TYPE == int(wtype), + CUSTOMWORDS.REGEX == int(regex)) \ + .order_by(CUSTOMWORDS.GROUP_ID).all() + return self._db.query(CUSTOMWORDS).all().order_by(CUSTOMWORDS.GROUP_ID) + + def is_custom_words_existed(self, replaced=None, front=None, back=None): + """ + 查询自定义识别词 + """ + if replaced: + count = self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.REPLACED == replaced).count() + elif front and back: + count = self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.FRONT == front, + CUSTOMWORDS.BACK == back).count() + else: + return False + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_custom_word_groups(self, title, year, gtype, tmdbid, season_count, note=None): + """ + 增加自定义识别词组 + """ + self._db.insert(CUSTOMWORDGROUPS( + TITLE=title, + YEAR=year, + TYPE=int(gtype), + TMDBID=int(tmdbid), + SEASON_COUNT=int(season_count), + NOTE=note + )) + + @DbPersist(_db) + def delete_custom_word_group(self, gid): + """ + 删除自定义识别词组 + """ + if not gid: + return + self._db.query(CUSTOMWORDS).filter(CUSTOMWORDS.GROUP_ID == int(gid)).delete() + self._db.query(CUSTOMWORDGROUPS).filter(CUSTOMWORDGROUPS.ID == int(gid)).delete() + + def get_custom_word_groups(self, gid=None, tmdbid=None, gtype=None): + """ + 查询自定义识别词组 + """ + if gid: + return self._db.query(CUSTOMWORDGROUPS).filter(CUSTOMWORDGROUPS.ID == int(gid)).all() + if tmdbid and gtype: + return self._db.query(CUSTOMWORDGROUPS).filter(CUSTOMWORDGROUPS.TMDBID == int(tmdbid), + CUSTOMWORDGROUPS.TYPE == int(gtype)).all() + return self._db.query(CUSTOMWORDGROUPS).all() + + def is_custom_word_group_existed(self, tmdbid=None, gtype=None): + """ + 查询自定义识别词组 + """ + if not gtype or not tmdbid: + return False + count = self._db.query(CUSTOMWORDGROUPS).filter(CUSTOMWORDGROUPS.TMDBID == int(tmdbid), + CUSTOMWORDGROUPS.TYPE == int(gtype)).count() + if count > 0: + return True + else: + return False + + @DbPersist(_db) + def insert_config_sync_path(self, source, dest, unknown, mode, rename, enabled, note=None): + """ + 增加目录同步 + """ + return self._db.insert(CONFIGSYNCPATHS( + SOURCE=source, + DEST=dest, + UNKNOWN=unknown, + MODE=mode, + RENAME=int(rename), + ENABLED=int(enabled), + NOTE=note + )) + + @DbPersist(_db) + def delete_config_sync_path(self, sid): + """ + 删除目录同步 + """ + if not sid: + return + self._db.query(CONFIGSYNCPATHS).filter(CONFIGSYNCPATHS.ID == int(sid)).delete() + + def get_config_sync_paths(self, sid=None): + """ + 查询目录同步 + """ + if sid: + return self._db.query(CONFIGSYNCPATHS).filter(CONFIGSYNCPATHS.ID == int(sid)).all() + return self._db.query(CONFIGSYNCPATHS).all() + + @DbPersist(_db) + def check_config_sync_paths(self, sid=None, source=None, rename=None, enabled=None): + """ + 设置目录同步状态 + """ + if sid and rename is not None: + self._db.query(CONFIGSYNCPATHS).filter(CONFIGSYNCPATHS.ID == int(sid)).update( + { + "RENAME": int(rename) + } + ) + elif sid and enabled is not None: + self._db.query(CONFIGSYNCPATHS).filter(CONFIGSYNCPATHS.ID == int(sid)).update( + { + "ENABLED": int(enabled) + } + ) + elif source and enabled is not None: + self._db.query(CONFIGSYNCPATHS).filter(CONFIGSYNCPATHS.SOURCE == source).update( + { + "ENABLED": int(enabled) + } + ) + + @DbPersist(_db) + def delete_download_setting(self, sid): + """ + 删除下载设置 + """ + if not sid: + return + self._db.query(DOWNLOADSETTING).filter(DOWNLOADSETTING.ID == int(sid)).delete() + + def get_download_setting(self, sid=None): + """ + 查询下载设置 + """ + if sid: + return self._db.query(DOWNLOADSETTING).filter(DOWNLOADSETTING.ID == int(sid)).all() + return self._db.query(DOWNLOADSETTING).all() + + @DbPersist(_db) + def update_download_setting(self, + sid, + name, + category, + tags, + content_layout, + is_paused, + upload_limit, + download_limit, + ratio_limit, + seeding_time_limit, + downloader): + """ + 设置下载设置 + """ + if sid: + self._db.query(DOWNLOADSETTING).filter(DOWNLOADSETTING.ID == int(sid)).update( + { + "NAME": name, + "CATEGORY": category, + "TAGS": tags, + "CONTENT_LAYOUT": int(content_layout), + "IS_PAUSED": int(is_paused), + "UPLOAD_LIMIT": int(float(upload_limit)), + "DOWNLOAD_LIMIT": int(float(download_limit)), + "RATIO_LIMIT": int(round(float(ratio_limit), 2) * 100), + "SEEDING_TIME_LIMIT": int(float(seeding_time_limit)), + "DOWNLOADER": downloader + } + ) + else: + self._db.insert(DOWNLOADSETTING( + NAME=name, + CATEGORY=category, + TAGS=tags, + CONTENT_LAYOUT=int(content_layout), + IS_PAUSED=int(is_paused), + UPLOAD_LIMIT=int(float(upload_limit)), + DOWNLOAD_LIMIT=int(float(download_limit)), + RATIO_LIMIT=int(round(float(ratio_limit), 2) * 100), + SEEDING_TIME_LIMIT=int(float(seeding_time_limit)), + DOWNLOADER=downloader + )) + + @DbPersist(_db) + def delete_message_client(self, cid): + """ + 删除消息服务器 + """ + if not cid: + return + self._db.query(MESSAGECLIENT).filter(MESSAGECLIENT.ID == int(cid)).delete() + + def get_message_client(self, cid=None): + """ + 查询消息服务器 + """ + if cid: + return self._db.query(MESSAGECLIENT).filter(MESSAGECLIENT.ID == int(cid)).all() + return self._db.query(MESSAGECLIENT).order_by(MESSAGECLIENT.TYPE).all() + + @DbPersist(_db) + def insert_message_client(self, + name, + ctype, + config, + switchs: list, + interactive, + enabled, + note=''): + """ + 设置消息服务器 + """ + self._db.insert(MESSAGECLIENT( + NAME=name, + TYPE=ctype, + CONFIG=config, + SWITCHS=json.dumps(switchs), + INTERACTIVE=int(interactive), + ENABLED=int(enabled), + NOTE=note + )) + + @DbPersist(_db) + def check_message_client(self, cid=None, interactive=None, enabled=None, ctype=None): + """ + 设置目录同步状态 + """ + if cid and interactive is not None: + self._db.query(MESSAGECLIENT).filter(MESSAGECLIENT.ID == int(cid)).update( + { + "INTERACTIVE": int(interactive) + } + ) + elif cid and enabled is not None: + self._db.query(MESSAGECLIENT).filter(MESSAGECLIENT.ID == int(cid)).update( + { + "ENABLED": int(enabled) + } + ) + elif not cid and int(interactive) == 0 and ctype: + self._db.query(MESSAGECLIENT).filter(MESSAGECLIENT.INTERACTIVE == 1, + MESSAGECLIENT.TYPE == ctype).update( + { + "INTERACTIVE": 0 + } + ) + + @DbPersist(_db) + def delete_torrent_remove_task(self, tid): + """ + 删除自动删种策略 + """ + if not tid: + return + self._db.query(TORRENTREMOVETASK).filter(TORRENTREMOVETASK.ID == int(tid)).delete() + + def get_torrent_remove_tasks(self, tid=None): + """ + 查询自动删种策略 + """ + if tid: + return self._db.query(TORRENTREMOVETASK).filter(TORRENTREMOVETASK.ID == int(tid)).all() + return self._db.query(TORRENTREMOVETASK).order_by(TORRENTREMOVETASK.NAME).all() + + @DbPersist(_db) + def insert_torrent_remove_task(self, + name, + action, + interval, + enabled, + samedata, + onlynastool, + downloader, + config: dict, + note=None): + """ + 设置自动删种策略 + """ + self._db.insert(TORRENTREMOVETASK( + NAME=name, + ACTION=int(action), + INTERVAL=int(interval), + ENABLED=int(enabled), + SAMEDATA=int(samedata), + ONLYNASTOOL=int(onlynastool), + DOWNLOADER=downloader, + CONFIG=json.dumps(config), + NOTE=note + )) + + @DbPersist(_db) + def delete_douban_history(self, hid): + """ + 删除豆瓣同步记录 + """ + if not hid: + return + self._db.query(DOUBANMEDIAS).filter(DOUBANMEDIAS.ID == int(hid)).delete() + + def get_douban_history(self): + """ + 查询豆瓣同步记录 + """ + return self._db.query(DOUBANMEDIAS).order_by(DOUBANMEDIAS.ADD_TIME.desc()).all() diff --git a/app/helper/dict_helper.py b/app/helper/dict_helper.py new file mode 100644 index 0000000..768d3c5 --- /dev/null +++ b/app/helper/dict_helper.py @@ -0,0 +1,79 @@ +from app.db import MainDb, DbPersist +from app.db.models import SYSTEMDICT + + +class DictHelper: + + _db = MainDb() + + @DbPersist(_db) + def set(self, dtype, key, value, note=""): + """ + 设置字典值 + :param dtype: 字典类型 + :param key: 字典Key + :param value: 字典值 + :param note: 备注 + :return: True False + """ + if not dtype or not key or not value: + return False + if self.exists(dtype, key): + return self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, + SYSTEMDICT.KEY == key).update( + { + "VALUE": value + } + ) + else: + return self._db.insert(SYSTEMDICT( + TYPE=dtype, + KEY=key, + VALUE=value, + NOTE=note + )) + + def get(self, dtype, key): + """ + 查询字典值 + :param dtype: 字典类型 + :param key: 字典Key + :return: 返回字典值 + """ + if not dtype or not key: + return "" + ret = self._db.query(SYSTEMDICT.VALUE).filter(SYSTEMDICT.TYPE == dtype, + SYSTEMDICT.KEY == key).first() + if ret: + return ret[0] + else: + return "" + + @DbPersist(_db) + def delete(self, dtype, key): + """ + 删除字典值 + :param dtype: 字典类型 + :param key: 字典Key + :return: True False + """ + if not dtype or not key: + return False + return self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, + SYSTEMDICT.KEY == key).delete() + + def exists(self, dtype, key): + """ + 查询字典是否存在 + :param dtype: 字典类型 + :param key: 字典Key + :return: True False + """ + if not dtype or not key: + return False + ret = self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, + SYSTEMDICT.KEY == key).count() + if ret > 0: + return True + else: + return False diff --git a/app/helper/display_helper.py b/app/helper/display_helper.py new file mode 100644 index 0000000..47f8004 --- /dev/null +++ b/app/helper/display_helper.py @@ -0,0 +1,43 @@ +import os + +from pyvirtualdisplay import Display + +from app.utils.commons import singleton +from app.utils import ExceptionUtils +from config import XVFB_PATH + + +@singleton +class DisplayHelper(object): + _display = None + + def __init__(self): + self.init_config() + + def init_config(self): + self.quit() + if self.can_display(): + try: + self._display = Display(visible=False, size=(1024, 768)) + self._display.start() + os.environ["NASTOOL_DISPLAY"] = "true" + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def get_display(self): + return self._display + + def quit(self): + os.environ["NASTOOL_DISPLAY"] = "" + if self._display: + self._display.stop() + + @staticmethod + def can_display(): + for path in XVFB_PATH: + if os.path.exists(path): + return True + return False + + def __del__(self): + self.quit() diff --git a/app/helper/ffmpeg_helper.py b/app/helper/ffmpeg_helper.py new file mode 100644 index 0000000..c75ba4f --- /dev/null +++ b/app/helper/ffmpeg_helper.py @@ -0,0 +1,19 @@ +from app.utils import SystemUtils + + +class FfmpegHelper: + + @staticmethod + def get_thumb_image_from_video(video_path, image_path, frames="00:03:01"): + """ + 使用ffmpeg从视频文件中截取缩略图 + """ + if not video_path or not image_path: + return False + cmd = 'ffmpeg -i "{video_path}" -ss {frames} -vframes 1 -f image2 "{image_path}"'.format(video_path=video_path, + frames=frames, + image_path=image_path) + result = SystemUtils.execute(cmd) + if result: + return True + return False diff --git a/app/helper/indexer_helper.py b/app/helper/indexer_helper.py new file mode 100644 index 0000000..82f31f9 --- /dev/null +++ b/app/helper/indexer_helper.py @@ -0,0 +1,113 @@ +import os.path +import pickle + +from app.utils import StringUtils, ExceptionUtils +from app.utils.commons import singleton +from config import Config + + +@singleton +class IndexerHelper: + _indexers = [] + + def __init__(self): + self.init_config() + + def init_config(self): + try: + with open(os.path.join(Config().get_inner_config_path(), + "sites.dat"), + "rb") as f: + self._indexers = pickle.load(f) + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def get_all_indexers(self): + return self._indexers + + def get_indexer(self, + url, + cookie=None, + name=None, + rule=None, + public=None, + proxy=False, + parser=None, + ua=None, + render=None, + language=None, + pri=None): + if not url: + return None + for indexer in self._indexers: + if not indexer.get("domain"): + continue + if StringUtils.url_equal(indexer.get("domain"), url): + return IndexerConf(datas=indexer, + cookie=cookie, + name=name, + rule=rule, + public=public, + proxy=proxy, + parser=parser, + ua=ua, + render=render, + builtin=True, + language=language, + pri=pri) + return None + + +class IndexerConf(object): + + def __init__(self, + datas=None, + cookie=None, + name=None, + rule=None, + public=None, + proxy=False, + parser=None, + ua=None, + render=None, + builtin=True, + language=None, + pri=None): + if not datas: + return + # ID + self.id = datas.get('id') + # 名称 + self.name = datas.get('name') if not name else name + # 是否内置站点 + self.builtin = builtin + # 域名 + self.domain = datas.get('domain') + # 搜索 + self.search = datas.get('search', {}) + # 批量搜索,如果为空对象则表示不支持批量搜索 + self.batch = self.search.get("batch", {}) if builtin else {} + # 解析器 + self.parser = parser if parser is not None else datas.get('parser') + # 是否启用渲染 + self.render = render if render is not None else datas.get("render") + # 浏览 + self.browse = datas.get('browse', {}) + # 种子过滤 + self.torrents = datas.get('torrents', {}) + # 分类 + self.category = datas.get('category', {}) + # Cookie + self.cookie = cookie + # User-Agent + self.ua = ua + # 过滤规则 + self.rule = rule + # 是否公开站点 + self.public = public + # 是否使用代理 + self.proxy = proxy + # 仅支持的特定语种 + self.language = language + # 索引器优先级 + self.pri = pri if pri else 0 diff --git a/app/helper/meta_helper.py b/app/helper/meta_helper.py new file mode 100644 index 0000000..0385703 --- /dev/null +++ b/app/helper/meta_helper.py @@ -0,0 +1,229 @@ +import os +import pickle +import random +import time +from enum import Enum +from threading import RLock + +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from config import Config + +lock = RLock() + +CACHE_EXPIRE_TIMESTAMP_STR = "cache_expire_timestamp" +EXPIRE_TIMESTAMP = 7 * 24 * 3600 + + +@singleton +class MetaHelper(object): + """ + { + "id": '', + "title": '', + "year": '', + "type": MediaType + } + """ + _meta_data = {} + + _meta_path = None + _tmdb_cache_expire = False + + def __init__(self): + self.init_config() + + def init_config(self): + laboratory = Config().get_config('laboratory') + if laboratory: + self._tmdb_cache_expire = laboratory.get("tmdb_cache_expire") + self._meta_path = os.path.join(Config().get_config_path(), 'tmdb.dat') + self._meta_data = self.__load_meta_data(self._meta_path) + + def clear_meta_data(self): + """ + 清空所有TMDB缓存 + """ + with lock: + self._meta_data = {} + + def get_meta_data_path(self): + """ + 返回TMDB缓存文件路径 + """ + return self._meta_path + + def get_meta_data_by_key(self, key): + """ + 根据KEY值获取缓存值 + """ + with lock: + info: dict = self._meta_data.get(key) + if info: + expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR) + if not expire or int(time.time()) < expire: + info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP + self.update_meta_data({key: info}) + elif expire and self._tmdb_cache_expire: + self.delete_meta_data(key) + return info or {} + + def dump_meta_data(self, search, page, num): + """ + 分页获取当前缓存列表 + @param search: 检索的缓存key + @param page: 页码 + @param num: 单页大小 + @return: 总数, 缓存列表 + """ + if page == 1: + begin_pos = 0 + else: + begin_pos = (page - 1) * num + + with lock: + search_metas = [(k, { + "id": v.get("id"), + "title": v.get("title"), + "year": v.get("year"), + "media_type": v.get("type").value if isinstance(v.get("type"), Enum) else v.get("type"), + "poster_path": v.get("poster_path"), + "backdrop_path": v.get("backdrop_path") + }, str(k).replace("[电影]", "").replace("[电视剧]", "").replace("[未知]", "").replace("-None", "")) + for k, v in self._meta_data.items() if search.lower() in k.lower() and v.get("id") != 0] + return len(search_metas), search_metas[begin_pos: begin_pos + num] + + def delete_meta_data(self, key): + """ + 删除缓存信息 + @param key: 缓存key + @return: 被删除的缓存内容 + """ + with lock: + return self._meta_data.pop(key, None) + + def delete_meta_data_by_tmdbid(self, tmdbid): + """ + 清空对应TMDBID的所有缓存记录,以强制更新TMDB中最新的数据 + """ + for key in list(self._meta_data): + if str(self._meta_data.get(key, {}).get("id")) == str(tmdbid): + with lock: + self._meta_data.pop(key) + + def delete_unknown_meta(self): + """ + 清除未识别的缓存记录,以便重新检索TMDB + """ + for key in list(self._meta_data): + if str(self._meta_data.get(key, {}).get("id")) == '0': + with lock: + self._meta_data.pop(key) + + def modify_meta_data(self, key, title): + """ + 删除缓存信息 + @param key: 缓存key + @param title: 标题 + @return: 被修改后缓存内容 + """ + with lock: + if self._meta_data.get(key): + self._meta_data[key]['title'] = title + self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP + return self._meta_data.get(key) + + @staticmethod + def __load_meta_data(path): + """ + 从文件中加载缓存 + """ + try: + if os.path.exists(path): + with open(path, 'rb') as f: + data = pickle.load(f) + return data + return {} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {} + + def update_meta_data(self, meta_data): + """ + 新增或更新缓存条目 + """ + if not meta_data: + return + with lock: + for key, item in meta_data.items(): + if not self._meta_data.get(key): + item[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP + self._meta_data[key] = item + + def save_meta_data(self, force=False): + """ + 保存缓存数据到文件 + """ + meta_data = self.__load_meta_data(self._meta_path) + new_meta_data = {k: v for k, v in self._meta_data.items() if str(v.get("id")) != '0'} + + if not force \ + and not self._random_sample(new_meta_data) \ + and meta_data.keys() == new_meta_data.keys(): + return + + with open(self._meta_path, 'wb') as f: + pickle.dump(new_meta_data, f, pickle.HIGHEST_PROTOCOL) + + def _random_sample(self, new_meta_data): + """ + 采样分析是否需要保存 + """ + ret = False + if len(new_meta_data) < 25: + keys = list(new_meta_data.keys()) + for k in keys: + info = new_meta_data.get(k) + expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR) + if not expire: + ret = True + info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP + elif int(time.time()) >= expire: + ret = True + if self._tmdb_cache_expire: + new_meta_data.pop(k) + else: + count = 0 + keys = random.sample(new_meta_data.keys(), 25) + for k in keys: + info = new_meta_data.get(k) + expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR) + if not expire: + ret = True + info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP + elif int(time.time()) >= expire: + ret = True + if self._tmdb_cache_expire: + new_meta_data.pop(k) + count += 1 + if count >= 5: + ret |= self._random_sample(new_meta_data) + return ret + + def get_cache_title(self, key): + """ + 获取缓存的标题 + """ + cache_media_info = self._meta_data.get(key) + if not cache_media_info or not cache_media_info.get("id"): + return None + return cache_media_info.get("title") + + def set_cache_title(self, key, cn_title): + """ + 重新设置缓存标题 + """ + cache_media_info = self._meta_data.get(key) + if not cache_media_info: + return + self._meta_data[key]['title'] = cn_title diff --git a/app/helper/ocr_helper.py b/app/helper/ocr_helper.py new file mode 100644 index 0000000..04dfa2f --- /dev/null +++ b/app/helper/ocr_helper.py @@ -0,0 +1,31 @@ +import base64 + +from app.utils import RequestUtils +from config import DEFAULT_OCR_SERVER + + +class OcrHelper: + req = None + _ocr_b64_url = "%s/captcha/base64" % DEFAULT_OCR_SERVER + + def __init__(self): + self.req = RequestUtils(content_type="application/json") + + def get_captcha_text(self, image_url=None, image_b64=None): + """ + 根据图片地址,获取验证码图片,并识别内容 + """ + if not image_url and not image_b64: + return "" + if image_url: + ret = self.req.get_res(image_url) + if ret is not None: + image_bin = ret.content + if not image_bin: + return "" + image_b64 = base64.b64encode(image_bin).decode() + ret = self.req.post_res(url=self._ocr_b64_url, + json={"base64_img": image_b64}) + if ret: + return ret.json().get("result") + return "" diff --git a/app/helper/opensubtitles.py b/app/helper/opensubtitles.py new file mode 100644 index 0000000..01cdb9e --- /dev/null +++ b/app/helper/opensubtitles.py @@ -0,0 +1,103 @@ +from functools import lru_cache +from urllib.parse import quote + +from pyquery import PyQuery + +import log +from app.helper.chrome_helper import ChromeHelper +from config import Config + + +class OpenSubtitles: + _cookie = "" + _ua = None + _url_imdbid = "https://www.opensubtitles.org/zh/search/imdbid-%s/sublanguageid-chi" + _url_keyword = "https://www.opensubtitles.org/zh/search/moviename-%s/sublanguageid-chi" + + def __init__(self): + self._ua = Config().get_ua() + + def search_subtitles(self, query): + if query.get("imdbid"): + return self.__search_subtitles_by_imdbid(query.get("imdbid")) + else: + return self.__search_subtitles_by_keyword("%s %s" % (query.get("name"), query.get("year"))) + + def __search_subtitles_by_imdbid(self, imdbid): + """ + 按TMDBID搜索OpenSubtitles + """ + return self.__parse_opensubtitles_results(url=self._url_imdbid % str(imdbid).replace("tt", "")) + + def __search_subtitles_by_keyword(self, keyword): + """ + 按关键字搜索OpenSubtitles + """ + return self.__parse_opensubtitles_results(url=self._url_keyword % quote(keyword)) + + @classmethod + @lru_cache(maxsize=128) + def __parse_opensubtitles_results(cls, url): + """ + 搜索并解析结果 + """ + chrome = ChromeHelper() + if not chrome.get_status(): + log.error("【Subtitle】未找到浏览器内核,当前环境无法检索opensubtitles字幕!") + return [] + # 访问页面 + if not chrome.visit(url): + log.error("【Subtitle】无法连接opensubtitles.org!") + return [] + # 源码 + html_text = chrome.get_html() + # Cookie + cls._cookie = chrome.get_cookies() + # 解析列表 + ret_subtitles = [] + html_doc = PyQuery(html_text) + global_season = '' + for tr in html_doc('#search_results > tbody > tr:not([style])'): + tr_doc = PyQuery(tr) + # 季 + season = tr_doc('span[id^="season-"] > a > b').text() + if season: + global_season = season + continue + # 集 + episode = tr_doc('span[itemprop="episodeNumber"]').text() + # 标题 + title = tr_doc('strong > a.bnone').text() + # 描述 下载链接 + if not global_season: + description = tr_doc('td:nth-child(1)').text() + if description and len(description.split("\n")) > 1: + description = description.split("\n")[1] + link = tr_doc('td:nth-child(5) > a').attr("href") + else: + description = tr_doc('span[itemprop="name"]').text() + link = tr_doc('a[href^="/download/"]').attr("href") + if link: + link = "https://www.opensubtitles.org%s" % link + else: + continue + ret_subtitles.append({ + "season": global_season, + "episode": episode, + "title": title, + "description": description, + "link": link + }) + return ret_subtitles + + def get_cookie(self): + """ + 返回Cookie + """ + return self._cookie + + def get_ua(self): + """ + 返回User-Agent + """ + return self._ua diff --git a/app/helper/progress_helper.py b/app/helper/progress_helper.py new file mode 100644 index 0000000..8bc388a --- /dev/null +++ b/app/helper/progress_helper.py @@ -0,0 +1,39 @@ +from app.utils.commons import singleton + + +@singleton +class ProgressHelper(object): + _process_detail = {} + + def __init__(self): + self._process_detail = {} + + def init_config(self): + pass + + def reset(self, ptype="search"): + self._process_detail[ptype] = { + "enable": False, + "value": 0, + "text": "请稍候..." + } + + def start(self, ptype="search"): + self.reset(ptype) + self._process_detail[ptype]['enable'] = True + + def end(self, ptype="search"): + if not self._process_detail.get(ptype): + return + self._process_detail[ptype]['enable'] = False + + def update(self, value=None, text=None, ptype="search"): + if not self._process_detail.get(ptype, {}).get('enable'): + return + if value: + self._process_detail[ptype]['value'] = value + if text: + self._process_detail[ptype]['text'] = text + + def get_process(self, ptype="search"): + return self._process_detail.get(ptype) diff --git a/app/helper/security_helper.py b/app/helper/security_helper.py new file mode 100644 index 0000000..cc6ec84 --- /dev/null +++ b/app/helper/security_helper.py @@ -0,0 +1,65 @@ +import ipaddress + +from app.utils import ExceptionUtils +from config import Config + + +class SecurityHelper: + media_server_webhook_allow_ip = {} + telegram_webhook_allow_ip = {} + synology_webhook_allow_ip = {} + + def __init__(self): + security = Config().get_config('security') + if security: + self.media_server_webhook_allow_ip = security.get('media_server_webhook_allow_ip') or {} + self.telegram_webhook_allow_ip = security.get('telegram_webhook_allow_ip') or {} + self.synology_webhook_allow_ip = security.get('synology_webhook_allow_ip') or {} + + def check_mediaserver_ip(self, ip): + return self.allow_access(self.media_server_webhook_allow_ip, ip) + + def check_telegram_ip(self, ip): + return self.allow_access(self.telegram_webhook_allow_ip, ip) + + def check_synology_ip(self, ip): + return self.allow_access(self.synology_webhook_allow_ip, ip) + + def check_slack_ip(self, ip): + return self.allow_access({"ipve": "127.0.0.1"}, ip) + + @staticmethod + def allow_access(allow_ips, ip): + """ + 判断IP是否合法 + :param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":} + :param ip: 需要检查的ip + """ + if not allow_ips: + return True + try: + ipaddr = ipaddress.ip_address(ip) + if ipaddr.version == 4: + if not allow_ips.get('ipv4'): + return True + allow_ipv4s = allow_ips.get('ipv4').split(",") + for allow_ipv4 in allow_ipv4s: + if ipaddr in ipaddress.ip_network(allow_ipv4): + return True + elif ipaddr.ipv4_mapped: + if not allow_ips.get('ipv4'): + return True + allow_ipv4s = allow_ips.get('ipv4').split(",") + for allow_ipv4 in allow_ipv4s: + if ipaddr.ipv4_mapped in ipaddress.ip_network(allow_ipv4): + return True + else: + if not allow_ips.get('ipv6'): + return True + allow_ipv6s = allow_ips.get('ipv6').split(",") + for allow_ipv6 in allow_ipv6s: + if ipaddr in ipaddress.ip_network(allow_ipv6): + return True + except Exception as e: + ExceptionUtils.exception_traceback(e) + return False diff --git a/app/helper/site_helper.py b/app/helper/site_helper.py new file mode 100644 index 0000000..85f5e08 --- /dev/null +++ b/app/helper/site_helper.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from lxml import etree + + +class SiteHelper: + @classmethod + def is_logged_in(cls, html_text): + """ + 判断站点是否已经登陆 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return False + # 存在明显的密码输入框,说明未登录 + if html.xpath("//input[@type='password']"): + return False + # 是否存在登出和用户面板等链接 + logout_or_usercp = html.xpath('//a[contains(@href, "logout") or contains(@data-url, "logout")' + ' or contains(@href, "mybonus") ' + ' or contains(@onclick, "logout") or contains(@href, "usercp")]') + + if logout_or_usercp: + return True + + user_info_div = html.xpath('//div[@class="user-info-side"]') + if user_info_div: + return True + + return False diff --git a/app/helper/submodule_helper.py b/app/helper/submodule_helper.py new file mode 100644 index 0000000..0518c79 --- /dev/null +++ b/app/helper/submodule_helper.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import importlib +import pkgutil + + +class SubmoduleHelper: + @classmethod + def import_submodules(cls, package, filter_func=lambda name, obj: True): + """ + 导入子模块 + :param package: 父包名 + :param filter_func: 子模块过滤函数,入参为模块名和模块对象,返回True则导入,否则不导入 + :return: + """ + + submodules = [] + packages = importlib.import_module(package).__path__ + for importer, package_name, _ in pkgutil.iter_modules(packages): + full_package_name = f'{package}.{package_name}' + if full_package_name.startswith('_'): + continue + module = importlib.import_module(full_package_name) + for name, obj in module.__dict__.items(): + if name.startswith('_'): + continue + if isinstance(obj, type) and filter_func(name, obj): + submodules.append(obj) + + return submodules diff --git a/app/helper/thread_helper.py b/app/helper/thread_helper.py new file mode 100644 index 0000000..5b1cedb --- /dev/null +++ b/app/helper/thread_helper.py @@ -0,0 +1,18 @@ +from concurrent.futures import ThreadPoolExecutor + +from app.utils.commons import singleton + + +@singleton +class ThreadHelper: + _thread_num = 50 + executor = None + + def __init__(self): + self.executor = ThreadPoolExecutor(max_workers=self._thread_num) + + def init_config(self): + pass + + def start_thread(self, func, kwargs): + self.executor.submit(func, *kwargs) diff --git a/app/helper/words_helper.py b/app/helper/words_helper.py new file mode 100644 index 0000000..b6c880e --- /dev/null +++ b/app/helper/words_helper.py @@ -0,0 +1,202 @@ +import regex as re + +from app.helper import DbHelper +from app.utils.commons import singleton +from app.utils.exception_utils import ExceptionUtils + + +@singleton +class WordsHelper: + dbhelper = None + ignored_words_info = [] + ignored_words_noregex_info = [] + replaced_words_info = [] + replaced_words_noregex_info = [] + replaced_offset_words_info = [] + offset_words_info = [] + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.ignored_words_info = self.dbhelper.get_custom_words(enabled=1, wtype=1, regex=1) + self.ignored_words_noregex_info = self.dbhelper.get_custom_words(enabled=1, wtype=1, regex=0) + self.replaced_words_info = self.dbhelper.get_custom_words(enabled=1, wtype=2, regex=1) + self.replaced_words_noregex_info = self.dbhelper.get_custom_words(enabled=1, wtype=2, regex=0) + self.replaced_offset_words_info = self.dbhelper.get_custom_words(enabled=1, wtype=3, regex=1) + self.offset_words_info = self.dbhelper.get_custom_words(enabled=1, wtype=4, regex=1) + + def process(self, title): + # 错误信息 + msg = [] + # 应用自定义识别 + used_ignored_words = [] + # 应用替换 + used_replaced_words = [] + # 应用集偏移 + used_offset_words = [] + # 屏蔽 + if self.ignored_words_info: + for ignored_word_info in self.ignored_words_info: + ignored = ignored_word_info.REPLACED + ignored_word = ignored + title, ignore_msg, ignore_flag = self.replace_regex(replaced=ignored, + replace="", + title=title) + if ignore_flag: + used_ignored_words.append(ignored_word) + elif ignore_msg: + msg.append(f"自定义屏蔽词 {ignored_word} 设置有误:{ignore_msg}") + if self.ignored_words_noregex_info: + for ignored_word_noregex_info in self.ignored_words_noregex_info: + ignored = ignored_word_noregex_info.REPLACED + ignored_word = ignored + title, ignore_msg, ignore_flag = self.replace_noregex(replaced=ignored, + replace="", + title=title) + if ignore_flag: + used_ignored_words.append(ignored_word) + elif ignore_msg: + msg.append(f"自定义屏蔽词 {ignored_word} 设置有误:{ignore_msg}") + # 替换 + if self.replaced_words_info: + for replaced_word_info in self.replaced_words_info: + replaced = replaced_word_info.REPLACED + replace = replaced_word_info.REPLACE + replaced_word = f"{replaced}@{replace}" + title, replace_msg, replace_flag = self.replace_regex(replaced=replaced, + replace=replace, + title=title) + if replace_flag: + used_replaced_words.append(replaced_word) + elif replace_msg: + msg.append(f"自定义替换词 {replaced_word} 格式有误:{replace_msg}") + if self.replaced_words_noregex_info: + for replaced_word_noregex_info in self.replaced_words_noregex_info: + replaced = replaced_word_noregex_info.REPLACED + replace = replaced_word_noregex_info.REPLACE + replaced_word = f"{replaced}@{replace}" + title, replace_msg, replace_flag = self.replace_noregex(replaced=replaced, + replace=replace, + title=title) + if replace_flag: + used_replaced_words.append(replaced_word) + elif replace_msg: + msg.append(f"自定义替换词 {replaced_word} 格式有误:{replace_msg}") + # 替换+集偏移 + if self.replaced_offset_words_info: + for replaced_offset_word_info in self.replaced_offset_words_info: + replaced = replaced_offset_word_info.REPLACED + replace = replaced_offset_word_info.REPLACE + front = replaced_offset_word_info.FRONT + back = replaced_offset_word_info.BACK + offset = replaced_offset_word_info.OFFSET + replaced_word = f"{replaced}@{replace}" + offset_word = f"{front}@{back}@{offset}" + replaced_offset_word = f"{replaced}@{replace}@{front}@{back}@{offset}" + # 替换 + title_replace, replace_msg, replace_flag = self.replace_regex(replaced=replaced, + replace=replace, + title=title) + # 替换应用成功进行集数偏移 + if replace_flag: + title_offset, offset_msg, offset_flag = self.episode_offset(front=front, + back=back, + offset=offset, + title=title_replace) + # 集数偏移应用成功 + if offset_flag: + used_replaced_words.append(replaced_word) + used_offset_words.append(offset_word) + title = title_offset + elif offset_msg: + msg.append(f"自定义替换+集偏移词 {replaced_offset_word} 集偏移部分格式有误:{offset_msg}") + elif replace_msg: + msg.append(f"自定义替换+集偏移词 {replaced_offset_word} 替换部分格式有误:{replace_msg}") + # 集数偏移 + if self.offset_words_info: + for offset_word_info in self.offset_words_info: + front = offset_word_info.FRONT + back = offset_word_info.BACK + offset = offset_word_info.OFFSET + offset_word = f"{front}@{back}@{offset}" + title, offset_msg, offset_flag = self.episode_offset(front, back, offset, title) + if offset_flag: + used_offset_words.append(offset_word) + elif offset_msg: + msg.append(f"自定义集偏移词 {offset_word} 格式有误:{offset_msg}") + + return title, msg, {"ignored": used_ignored_words, + "replaced": used_replaced_words, + "offset": used_offset_words} + + @staticmethod + def replace_regex(replaced, replace, title): + msg = "" + try: + if not re.findall(r'%s' % replaced, title): + return title, msg, False + else: + title = re.sub(r'%s' % replaced, r'%s' % replace, title) + return title, msg, True + except Exception as err: + ExceptionUtils.exception_traceback(err) + msg = str(err) + return title, msg, False + + @staticmethod + def replace_noregex(replaced, replace, title): + msg = "" + try: + if title.find(replaced) == -1: + return title, msg, False + else: + title = title.replace(replaced, replace) + return title, msg, True + except Exception as err: + ExceptionUtils.exception_traceback(err) + msg = str(err) + return title, msg, False + + @staticmethod + def episode_offset(front, back, offset, title): + msg = "" + try: + if back and not re.findall(r'%s' % back, title): + return title, msg, False + if front and not re.findall(r'%s' % front, title): + return title, msg, False + offset_word_info_re = re.compile(r'(?<=%s.*?)[0-9]+(?=.*?%s)' % (front, back)) + episode_nums_str = re.findall(offset_word_info_re, title) + if not episode_nums_str: + return title, msg, False + episode_nums_offset_int = [] + offset_order_flag = False + for episode_num_str in episode_nums_str: + episode_num_int = int(episode_num_str) + offset_caculate = offset.replace("EP", str(episode_num_int)) + episode_num_offset_int = eval(offset_caculate) + # 向前偏移 + if episode_num_int > episode_num_offset_int: + offset_order_flag = True + # 向后偏移 + else: + offset_order_flag = False + episode_nums_offset_int.append(episode_num_offset_int) + episode_nums_dict = dict(zip(episode_nums_str, episode_nums_offset_int)) + # 集数向前偏移,集数按升序处理 + if offset_order_flag: + episode_nums_list = sorted(episode_nums_dict.items(), key=lambda x: x[1]) + # 集数向后偏移,集数按降序处理 + else: + episode_nums_list = sorted(episode_nums_dict.items(), key=lambda x: x[1], reverse=True) + for episode_num in episode_nums_list: + episode_offset_re = re.compile( + r'(?<=%s.*?)%s(?=.*?%s)' % (front, episode_num[0], back)) + title = re.sub(episode_offset_re, r'%s' % str(episode_num[1]).zfill(2), title) + return title, msg, True + except Exception as err: + ExceptionUtils.exception_traceback(err) + msg = str(err) + return title, msg, False diff --git a/app/indexer/__init__.py b/app/indexer/__init__.py new file mode 100644 index 0000000..256eaa1 --- /dev/null +++ b/app/indexer/__init__.py @@ -0,0 +1 @@ +from .indexer import Indexer diff --git a/app/indexer/client/__init__.py b/app/indexer/client/__init__.py new file mode 100644 index 0000000..370dd6f --- /dev/null +++ b/app/indexer/client/__init__.py @@ -0,0 +1 @@ +from .builtin import BuiltinIndexer diff --git a/app/indexer/client/_base.py b/app/indexer/client/_base.py new file mode 100644 index 0000000..3f0255f --- /dev/null +++ b/app/indexer/client/_base.py @@ -0,0 +1,229 @@ +import datetime +import xml.dom.minidom +from abc import ABCMeta, abstractmethod + +import log +from app.filter import Filter +from app.helper import ProgressHelper +from app.media import Media +from app.media.meta import MetaInfo +from app.utils import DomUtils, RequestUtils, StringUtils, ExceptionUtils +from app.utils.types import MediaType, SearchType + + +class _IIndexClient(metaclass=ABCMeta): + media = None + index_type = None + api_key = None + host = None + filter = None + progress = None + _reverse_title_sites = ['keepfriends'] + + def __init__(self): + self.media = Media() + self.filter = Filter() + self.progress = ProgressHelper() + + @abstractmethod + def match(self, ctype): + """ + 匹配实例 + """ + pass + + @abstractmethod + def get_status(self): + """ + 检查连通性 + """ + pass + + @abstractmethod + def get_indexers(self): + """ + :return: indexer 信息 [(indexerId, indexerName, url)] + """ + pass + + @abstractmethod + def search(self, order_seq, + indexer, + key_word, + filter_args: dict, + match_media, + in_from: SearchType): + """ + 根据关键字多线程检索 + """ + pass + + def filter_search_results(self, result_array: list, + order_seq, + indexer, + filter_args: dict, + match_media, + start_time): + """ + 从检索结果中匹配符合资源条件的记录 + """ + ret_array = [] + index_sucess = 0 + index_rule_fail = 0 + index_match_fail = 0 + index_error = 0 + for item in result_array: + # 这此站标题和副标题相反 + if indexer.id in self._reverse_title_sites: + torrent_name = item.get('description') + description = item.get('title') + else: + torrent_name = item.get('title') + description = item.get('description') + if not torrent_name: + index_error += 1 + continue + enclosure = item.get('enclosure') + size = item.get('size') + seeders = item.get('seeders') + peers = item.get('peers') + page_url = item.get('page_url') + uploadvolumefactor = round(float(item.get('uploadvolumefactor')), 1) if item.get( + 'uploadvolumefactor') is not None else 1.0 + downloadvolumefactor = round(float(item.get('downloadvolumefactor')), 1) if item.get( + 'downloadvolumefactor') is not None else 1.0 + imdbid = item.get("imdbid") + # 全匹配模式下,非公开站点,过滤掉做种数为0的 + if filter_args.get("seeders") and not indexer.public and str(seeders) == "0": + log.info(f"【{self.index_type}】{torrent_name} 做种数为0") + index_rule_fail += 1 + continue + # 识别种子名称 + meta_info = MetaInfo(title=torrent_name, subtitle=description) + if not meta_info.get_name(): + log.info(f"【{self.index_type}】{torrent_name} 无法识别到名称") + index_match_fail += 1 + continue + # 大小及促销等 + meta_info.set_torrent_info(size=size, + imdbid=imdbid, + upload_volume_factor=uploadvolumefactor, + download_volume_factor=downloadvolumefactor) + + # 先过滤掉可以明确的类型 + if meta_info.type == MediaType.TV and filter_args.get("type") == MediaType.MOVIE: + log.info( + f"【{self.index_type}】{torrent_name} 是 {meta_info.type.value},不匹配类型:{filter_args.get('type').value}") + index_rule_fail += 1 + continue + # 检查订阅过滤规则匹配 + match_flag, res_order, match_msg = self.filter.check_torrent_filter(meta_info=meta_info, + filter_args=filter_args, + uploadvolumefactor=uploadvolumefactor, + downloadvolumefactor=downloadvolumefactor) + if not match_flag: + log.info(f"【{self.index_type}】{match_msg}") + index_rule_fail += 1 + continue + # 识别媒体信息 + if not match_media: + # 不过滤 + media_info = meta_info + else: + # 0-识别并模糊匹配;1-识别并精确匹配 + if meta_info.imdb_id \ + and match_media.imdb_id \ + and str(meta_info.imdb_id) == str(match_media.imdb_id): + # IMDBID匹配,合并媒体数据 + media_info = self.media.merge_media_info(meta_info, match_media) + else: + # 查询缓存 + cache_info = self.media.get_cache_info(meta_info) + if match_media \ + and str(cache_info.get("id")) == str(match_media.tmdb_id): + # 缓存匹配,合并媒体数据 + media_info = self.media.merge_media_info(meta_info, match_media) + else: + # 重新识别 + media_info = self.media.get_media_info(title=torrent_name, subtitle=description, chinese=False) + if not media_info: + log.warn(f"【{self.index_type}】{torrent_name} 识别媒体信息出错!") + index_error += 1 + continue + elif not media_info.tmdb_info: + log.info( + f"【{self.index_type}】{torrent_name} 识别为 {media_info.get_name()} 未匹配到媒体信息") + index_match_fail += 1 + continue + # TMDBID是否匹配 + if str(media_info.tmdb_id) != str(match_media.tmdb_id): + log.info( + f"【{self.index_type}】{torrent_name} 识别为 {media_info.type.value} {media_info.get_title_string()} 不匹配") + index_match_fail += 1 + continue + # 合并媒体数据 + media_info = self.media.merge_media_info(media_info, match_media) + # 过滤类型 + if filter_args.get("type"): + if (filter_args.get("type") == MediaType.TV and media_info.type == MediaType.MOVIE) \ + or (filter_args.get("type") == MediaType.MOVIE and media_info.type == MediaType.TV): + log.info( + f"【{self.index_type}】{torrent_name} 是 {media_info.type.value},不是 {filter_args.get('type').value}") + index_rule_fail += 1 + continue + # 洗版 + if match_media.over_edition: + # 季集不完整的资源不要 + if media_info.type != MediaType.MOVIE \ + and media_info.get_episode_list(): + log.info(f"【{self.index_type}】{media_info.get_title_string()}{media_info.get_season_string()} " + f"正在洗版,过滤掉季集不完整的资源:{torrent_name} {description}") + continue + # 检查优先级是否更好 + if match_media.res_order \ + and int(res_order) <= int(match_media.res_order): + log.info( + f"【{self.index_type}】{media_info.get_title_string()}{media_info.get_season_string()} " + f"正在洗版,已洗版优先级:{100 - int(match_media.res_order)}," + f"当前资源优先级:{100 - int(res_order)}," + f"跳过低优先级或同优先级资源:{torrent_name}" + ) + continue + # 检查标题是否匹配季、集、年 + if not self.filter.is_torrent_match_sey(media_info, + filter_args.get("season"), + filter_args.get("episode"), + filter_args.get("year")): + log.info( + f"【{self.index_type}】{torrent_name} 识别为 {media_info.type.value} {media_info.get_title_string()} {media_info.get_season_episode_string()} 不匹配季/集/年份") + index_match_fail += 1 + continue + + # 匹配到了 + log.info( + f"【{self.index_type}】{torrent_name} {description} 识别为 {media_info.get_title_string()} {media_info.get_season_episode_string()} 匹配成功") + media_info.set_torrent_info(site=indexer.name, + site_order=order_seq, + enclosure=enclosure, + res_order=res_order, + filter_rule=filter_args.get("rule"), + size=size, + seeders=seeders, + peers=peers, + description=description, + page_url=page_url, + upload_volume_factor=uploadvolumefactor, + download_volume_factor=downloadvolumefactor) + if media_info not in ret_array: + index_sucess += 1 + ret_array.append(media_info) + else: + index_rule_fail += 1 + # 循环结束 + # 计算耗时 + end_time = datetime.datetime.now() + log.info( + f"【{self.index_type}】{indexer.name} 共检索到 {len(result_array)} 条数据,过滤 {index_rule_fail},不匹配 {index_match_fail},错误 {index_error},有效 {index_sucess},耗时 {(end_time - start_time).seconds} 秒") + self.progress.update(ptype='search', + text=f"{indexer.name} 共检索到 {len(result_array)} 条数据,过滤 {index_rule_fail},不匹配 {index_match_fail},错误 {index_error},有效 {index_sucess},耗时 {(end_time - start_time).seconds} 秒") + return ret_array diff --git a/app/indexer/client/_rarbg.py b/app/indexer/client/_rarbg.py new file mode 100644 index 0000000..e71e553 --- /dev/null +++ b/app/indexer/client/_rarbg.py @@ -0,0 +1,66 @@ +import requests + +import log +from app.utils import RequestUtils +from config import Config + + +class Rarbg: + _appid = "nastool" + _req = None + _token = None + _api_url = "http://torrentapi.org/pubapi_v2.php" + + def __init__(self): + self.init_config() + + def init_config(self): + session = requests.session() + self._req = RequestUtils(proxies=Config().get_proxies(), session=session, timeout=10) + self.__get_token() + + def __get_token(self): + if self._token: + return + res = self._req.get_res(url=self._api_url, params={'app_id': self._appid, 'get_token': 'get_token'}) + if res and res.json(): + self._token = res.json().get('token') + + def search(self, keyword, indexer, imdb_id=None): + if not keyword: + return [] + self.__get_token() + if not self._token: + log.warn(f"【INDEXER】{indexer.name} 未获取到token,无法搜索") + return [] + params = {'app_id': self._appid, 'mode': 'search', 'token': self._token, 'format': 'json_extended', 'limit': 100} + if imdb_id: + params['search_imdb'] = imdb_id + else: + params['search_string'] = keyword + res = self._req.get_res(url=self._api_url, params=params) + torrents = [] + if res and res.status_code == 200: + results = res.json().get('torrent_results') or [] + for result in results: + if not result or not result.get('title'): + continue + torrent = {'indexer': indexer.id, + 'title': result.get('title'), + 'enclosure': result.get('download'), + 'size': result.get('size'), + 'seeders': result.get('seeders'), + 'peers': result.get('leechers'), + 'freeleech': True, + 'downloadvolumefactor': 0.0, + 'uploadvolumefactor': 1.0, + 'page_url': result.get('info_page'), + 'imdbid': result.get('episode_info').get('imdb') if result.get('episode_info') else ''} + torrents.append(torrent) + elif res is not None: + log.warn(f"【INDEXER】{indexer.name} 搜索失败,错误码:{res.status_code}") + return [] + else: + log.warn(f"【INDEXER】{indexer.name} 搜索失败,无法连接 torrentapi.org") + return [] + return torrents diff --git a/app/indexer/client/_render_spider.py b/app/indexer/client/_render_spider.py new file mode 100644 index 0000000..96b8702 --- /dev/null +++ b/app/indexer/client/_render_spider.py @@ -0,0 +1,129 @@ +# coding: utf-8 +import copy +import time +from urllib.parse import quote + +from pyquery import PyQuery +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as es +from selenium.webdriver.support.wait import WebDriverWait + +from app.helper import ChromeHelper +from app.indexer.client._spider import TorrentSpider +from app.utils import ExceptionUtils +from config import Config + + +class RenderSpider(object): + + torrentspider = None + torrents_info_array = [] + result_num = 100 + + def __init__(self): + self.torrentspider = TorrentSpider() + self.init_config() + + def init_config(self): + self.torrents_info_array = [] + self.result_num = Config().get_config('pt').get('site_search_result_num') or 100 + + def search(self, keyword, indexer, page=None, mtype=None): + """ + 开始搜索 + """ + + if not indexer: + return [] + if not keyword: + keyword = "" + if isinstance(keyword, list): + keyword = " ".join(keyword) + chrome = ChromeHelper() + if not chrome.get_status(): + return [] + # 请求路径 + torrentspath = indexer.search.get('paths', [{}])[0].get('path', '') or '' + search_url = indexer.domain + torrentspath.replace("{keyword}", quote(keyword)) + # 请求方式,支持GET和浏览仿真 + method = indexer.search.get('paths', [{}])[0].get('method', '') + if method == "chrome": + # 请求参数 + params = indexer.search.get('paths', [{}])[0].get('params', {}) + # 搜索框 + search_input = params.get('keyword') + # 搜索按钮 + search_button = params.get('submit') + # 预执行脚本 + pre_script = params.get('script') + # referer + if params.get('referer'): + referer = indexer.domain + params.get('referer').replace('{keyword}', quote(keyword)) + else: + referer = indexer.domain + if not search_input or not search_button: + return [] + # 使用浏览器打开页面 + if not chrome.visit(url=search_url, + cookie=indexer.cookie, + ua=indexer.ua): + return [] + cloudflare = chrome.pass_cloudflare() + if not cloudflare: + return [] + # 模拟搜索操作 + try: + # 执行脚本 + if pre_script: + chrome.execute_script(pre_script) + # 等待可点击 + submit_obj = WebDriverWait(driver=chrome.browser, + timeout=10).until(es.element_to_be_clickable((By.XPATH, + search_button))) + if submit_obj: + # 输入用户名 + chrome.browser.find_element(By.XPATH, search_input).send_keys(keyword) + # 提交搜索 + submit_obj.click() + else: + return [] + except Exception as e: + ExceptionUtils.exception_traceback(e) + return [] + else: + # referer + referer = indexer.domain + # 使用浏览器获取HTML文本 + if not chrome.visit(url=search_url, + cookie=indexer.cookie, + ua=indexer.ua): + return [] + cloudflare = chrome.pass_cloudflare() + if not cloudflare: + return [] + # 等待页面加载完成 + time.sleep(5) + # 获取HTML文本 + html_text = chrome.get_html() + if not html_text: + return [] + # 重新获取Cookie和UA + indexer.cookie = chrome.get_cookies() + indexer.ua = chrome.get_ua() + # 设置抓虫参数 + self.torrentspider.setparam(keyword=keyword, + indexer=indexer, + referer=referer, + page=page, + mtype=mtype) + # 种子筛选器 + torrents_selector = indexer.torrents.get('list', {}).get('selector', '') + if not torrents_selector: + return [] + # 解析HTML文本 + html_doc = PyQuery(html_text) + for torn in html_doc(torrents_selector): + self.torrents_info_array.append(copy.deepcopy(self.torrentspider.Getinfo(PyQuery(torn)))) + if len(self.torrents_info_array) >= int(self.result_num): + break + return self.torrents_info_array diff --git a/app/indexer/client/_spider.py b/app/indexer/client/_spider.py new file mode 100644 index 0000000..1258fbf --- /dev/null +++ b/app/indexer/client/_spider.py @@ -0,0 +1,650 @@ +import copy +import datetime +import re +from urllib.parse import quote + +from jinja2 import Template +from pyquery import PyQuery + +import feapder +import log +from app.utils import StringUtils, SystemUtils +from app.utils.exception_utils import ExceptionUtils +from app.utils.types import MediaType +from config import Config +from feapder.utils.tools import urlencode + + +class TorrentSpider(feapder.AirSpider): + _webdriver_path = SystemUtils.get_webdriver_path() + __custom_setting__ = dict( + USE_SESSION=True, + SPIDER_THREAD_COUNT=1, + SPIDER_MAX_RETRY_TIMES=0, + REQUEST_LOST_TIMEOUT=10, + RETRY_FAILED_REQUESTS=False, + LOG_LEVEL="ERROR", + RANDOM_HEADERS=False, + WEBDRIVER=dict( + pool_size=1, + load_images=False, + proxy=None, + headless=True, + driver_type="CHROME", + timeout=20, + window_size=(1024, 800), + executable_path=_webdriver_path, + render_time=10, + custom_argument=["--ignore-certificate-errors"], + ) + ) + # 是否检索完成标志 + is_complete = False + # 索引器ID + indexerid = None + # 索引器名称 + indexername = None + # 站点域名 + domain = None + # 站点Cookie + cookie = None + # 站点UA + ua = None + # 代理 + proxies = None + # 是否渲染 + render = False + # Referer + referer = None + # 检索关键字 + keyword = None + # 媒体类型 + mtype = None + # 检索路径、方式配置 + search = {} + # 批量检索配置 + batch = {} + # 浏览配置 + browse = {} + # 站点分类配置 + category = {} + # 站点种子列表配置 + list = {} + # 站点种子字段配置 + fields = {} + # 页码 + page = 0 + # 检索条数 + result_num = 100 + torrents_info = {} + torrents_info_array = [] + + def setparam(self, indexer, + keyword: [str, list] = None, + page=None, + referer=None, + mtype: MediaType = None): + """ + 设置查询参数 + :param indexer: 索引器 + :param keyword: 检索关键字,如果数组则为批量检索 + :param page: 页码 + :param referer: Referer + :param mtype: 媒体类型 + """ + if not indexer: + return + self.keyword = keyword + self.mtype = mtype + self.indexerid = indexer.id + self.indexername = indexer.name + self.search = indexer.search + self.batch = indexer.batch + self.browse = indexer.browse + self.category = indexer.category + self.list = indexer.torrents.get('list', {}) + self.fields = indexer.torrents.get('fields') + self.render = indexer.render + self.domain = indexer.domain + self.page = page + if self.domain and not str(self.domain).endswith("/"): + self.domain = self.domain + "/" + if indexer.ua: + self.ua = indexer.ua + else: + self.ua = Config().get_ua() + if indexer.proxy: + self.proxies = Config().get_proxies() + if indexer.cookie: + self.cookie = indexer.cookie + if referer: + self.referer = referer + self.result_num = Config().get_config('pt').get('site_search_result_num') or 100 + self.torrents_info_array = [] + + def start_requests(self): + """ + 开始请求 + """ + + if not self.search or not self.domain: + self.is_complete = True + return + + # 种子搜索相对路径 + paths = self.search.get('paths', []) + torrentspath = "" + if len(paths) == 1: + torrentspath = paths[0].get('path', '') + else: + for path in paths: + if path.get("type") == "all" and not self.mtype: + torrentspath = path.get('path') + break + elif path.get("type") == "movie" and self.mtype == MediaType.MOVIE: + torrentspath = path.get('path') + break + elif path.get("type") == "tv" and self.mtype == MediaType.TV: + torrentspath = path.get('path') + break + elif path.get("type") == "anime" and self.mtype == MediaType.ANIME: + torrentspath = path.get('path') + break + + # 关键字搜索 + if self.keyword: + + if isinstance(self.keyword, list): + # 批量查询 + if self.batch: + delimiter = self.batch.get('delimiter') or ' ' + space_replace = self.batch.get('space_replace') or ' ' + search_word = delimiter.join([str(k).replace(' ', space_replace) for k in self.keyword]) + else: + search_word = " ".join(self.keyword) + # 查询模式:或 + search_mode = "1" + else: + # 单个查询 + search_word = self.keyword + # 查询模式与 + search_mode = "0" + + # 检索URL + if self.search.get("params"): + # 变量字典 + inputs_dict = { + "keyword": search_word + } + # 查询参数 + params = { + "search_mode": search_mode, + "page": self.page or 0 + } + # 额外参数 + for key, value in self.search.get("params").items(): + params.update({ + "%s" % key: str(value).format(**inputs_dict) + }) + # 分类条件 + if self.category: + if self.mtype == MediaType.MOVIE: + cats = self.category.get("movie") or [] + elif self.mtype: + cats = self.category.get("tv") or [] + else: + cats = self.category.get("movie") or [] + self.category.get("tv") or [] + for cat in cats: + if self.category.get("field"): + value = params.get(self.category.get("field"), "") + params.update({ + "%s" % self.category.get("field"): value + self.category.get("delimiter", ' ') + cat.get("id") + }) + else: + params.update({ + "%s" % cat.get("id"): 1 + }) + searchurl = self.domain + torrentspath + "?" + urlencode(params) + else: + # 变量字典 + inputs_dict = { + "keyword": quote(search_word), + "page": self.page or 0 + } + # 无额外参数 + searchurl = self.domain + str(torrentspath).format(**inputs_dict) + + # 列表浏览 + else: + # 变量字典 + inputs_dict = { + "page": self.page or 0, + "keyword": "" + } + # 有单独浏览路径 + if self.browse: + torrentspath = self.browse.get("path") + if self.browse.get("start"): + inputs_dict.update({ + "page": self.browse.get("start") + }) + elif self.page: + torrentspath = torrentspath + f"?page={self.page}" + # 检索Url + searchurl = self.domain + str(torrentspath).format(**inputs_dict) + + log.info(f"【Spider】开始请求:{searchurl}") + yield feapder.Request(url=searchurl, + use_session=True, + render=self.render) + + def download_midware(self, request): + request.headers = { + "User-Agent": self.ua, + "Cookie": self.cookie + } + if self.proxies: + request.proxies = self.proxies + return request + + def Gettitle_default(self, torrent): + # title default + if 'title' not in self.fields: + return + selector = self.fields.get('title', {}) + if 'selector' in selector: + title = torrent(selector.get('selector', '')).clone() + if "remove" in selector: + removelist = selector.get('remove', '').split(', ') + for v in removelist: + title.remove(v) + if 'attribute' in selector: + items = [item.attr(selector.get('attribute')) for item in title.items() if item] + else: + items = [item.text() for item in title.items() if item] + if items: + if "contents" in selector \ + and len(items) > int(selector.get("contents")): + items = items[0].split("\n")[selector.get("contents")] + elif "index" in selector \ + and len(items) > int(selector.get("index")): + items = items[int(selector.get("index"))] + else: + items = items[0] + self.torrents_info['title'] = items if not isinstance(items, list) else items[0] + elif 'text' in selector: + render_dict = {} + if "title_default" in self.fields: + title_default_selector = self.fields.get('title_default', {}) + title_default_item = torrent(title_default_selector.get('selector', '')).clone() + if "remove" in title_default_selector: + removelist = title_default_selector.get('remove', '').split(', ') + for v in removelist: + title_default_item.remove(v) + if 'attribute' in title_default_selector: + render_dict.update( + {'title_default': title_default_item.attr(title_default_selector.get('attribute'))}) + else: + render_dict.update({'title_default': title_default_item.text()}) + if "title_optional" in self.fields: + title_optional_selector = self.fields.get('title_optional', {}) + title_optional_item = torrent(title_optional_selector.get('selector', '')).clone() + if "remove" in title_optional_selector: + removelist = title_optional_selector.get('remove', '').split(', ') + for v in removelist: + title_optional_item.remove(v) + if 'attribute' in title_optional_selector: + render_dict.update( + {'title_optional': title_optional_item.attr(title_optional_selector.get('attribute'))}) + else: + render_dict.update({'title_optional': title_optional_item.text()}) + self.torrents_info['title'] = Template(selector.get('text')).render(fields=render_dict) + if 'filters' in selector: + self.torrents_info['title'] = self.__filter_text(self.torrents_info.get('title'), + selector.get('filters')) + + def Gettitle_optional(self, torrent): + # title optional + if 'description' not in self.fields: + return + selector = self.fields.get('description', {}) + if "selector" in selector \ + or "selectors" in selector: + description = torrent(selector.get('selector', selector.get('selectors', ''))).clone() + if description: + if 'remove' in selector: + removelist = selector.get('remove', '').split(', ') + for v in removelist: + description.remove(v) + if 'attribute' in selector: + items = [x.attr(selector.get('attribute')) for x in description.items()] + else: + items = [item.text() for item in description.items() if item] + if items: + if "contents" in selector \ + and len(items) > int(selector.get("contents")): + items = items[0].split("\n")[selector.get("contents")] + elif "index" in selector \ + and len(items) > int(selector.get("index")): + items = items[int(selector.get("index"))] + else: + items = items[0] + self.torrents_info['description'] = items if not isinstance(items, list) else items[0] + elif "text" in selector: + render_dict = {} + if "tags" in self.fields: + tags_selector = self.fields.get('tags', {}) + tags_item = torrent(tags_selector.get('selector', '')).clone() + if "remove" in tags_selector: + removelist = tags_selector.get('remove', '').split(', ') + for v in removelist: + tags_item.remove(v) + render_dict.update({'tags': tags_item.text()}) + if "subject" in self.fields: + subject_selector = self.fields.get('subject', {}) + subject_item = torrent(subject_selector.get('selector', '')).clone() + if "remove" in subject_selector: + removelist = subject_selector.get('remove', '').split(', ') + for v in removelist: + subject_item.remove(v) + render_dict.update({'subject': subject_item.text()}) + if "description_free_forever" in self.fields: + render_dict.update({"description_free_forever": torrent(self.fields.get("description_free_forever", + {}).get("selector", + '')).text()}) + if "description_normal" in self.fields: + render_dict.update({"description_normal": torrent(self.fields.get("description_normal", + {}).get("selector", + '')).text()}) + self.torrents_info['description'] = Template(selector.get('text')).render(fields=render_dict) + if 'filters' in selector: + self.torrents_info['description'] = self.__filter_text(self.torrents_info.get('description'), + selector.get('filters')) + + def Getdetails(self, torrent): + # details + if 'details' not in self.fields: + return + details = torrent(self.fields.get('details', {}).get('selector', '')) + items = [item.attr(self.fields.get('details', {}).get('attribute')) for item in details.items()] + if items: + if not items[0].startswith("http"): + if items[0].startswith("//"): + self.torrents_info['page_url'] = self.domain.split(":")[0] + ":" + items[0] + elif items[0].startswith("/"): + self.torrents_info['page_url'] = self.domain + items[0][1:] + else: + self.torrents_info['page_url'] = self.domain + items[0] + else: + self.torrents_info['page_url'] = items[0] + if 'filters' in self.fields.get('details', {}): + self.torrents_info['page_url'] = self.__filter_text(self.torrents_info.get('page_url'), + self.fields.get('details', + {}).get('filters')) + + def Getdownload(self, torrent): + # download link + if 'download' not in self.fields: + return + if "detail" in self.fields.get('download', {}): + selector = self.fields.get('download', {}).get("detail", {}) + if "xpath" in selector: + self.torrents_info['enclosure'] = f'[{selector.get("xpath", "")}' \ + f'|{self.cookie or ""}' \ + f'|{self.ua or ""}' \ + f'|{self.referer or ""}]' + elif "hash" in selector: + self.torrents_info['enclosure'] = f'#{selector.get("hash", "")}' \ + f'|{self.cookie or ""}' \ + f'|{self.ua or ""}' \ + f'|{self.referer or ""}#' + else: + download = torrent(self.fields.get('download', {}).get('selector', '')) + items = [item.attr(self.fields.get('download', {}).get('attribute')) for item in download.items()] + if items: + if not items[0].startswith("http") and not items[0].startswith("magnet"): + self.torrents_info['enclosure'] = self.domain + items[0][1:] if items[0].startswith( + "/") else self.domain + items[0] + else: + self.torrents_info['enclosure'] = items[0] + + def Getimdbid(self, torrent): + # imdbid + if "imdbid" not in self.fields: + return + selector = self.fields.get('imdbid', {}) + imdbid = torrent(selector.get('selector', '')) + if 'attribute' in selector: + items = [item.attr(selector.get('attribute')) for item in imdbid.items() if item] + else: + items = [item.text() for item in imdbid.items() if item] + self.torrents_info['imdbid'] = items[0] if items else '' + if 'filters' in selector: + self.torrents_info['imdbid'] = self.__filter_text(self.torrents_info.get('imdbid'), + selector.get('filters')) + + def Getsize(self, torrent): + # torrent size + if 'size' not in self.fields: + return + selector = self.fields.get('size', {}) + size = torrent(selector.get('selector', selector.get("selectors", ''))) + items = [item.text() for item in size.items() if item] + if "index" in selector \ + and len(items) > selector.get('index'): + self.torrents_info['size'] = StringUtils.num_filesize( + items[selector.get('index')].replace("\n", "").strip()) + elif len(items) > 0: + self.torrents_info['size'] = StringUtils.num_filesize( + items[0].replace("\n", "").strip()) + if 'filters' in selector: + self.torrents_info['size'] = self.__filter_text(self.torrents_info.get('size'), + selector.get('filters')) + if self.torrents_info.get('size'): + self.torrents_info['size'] = StringUtils.num_filesize(self.torrents_info.get('size')) + + def Getleechers(self, torrent): + # torrent leechers + if 'leechers' not in self.fields: + return + selector = self.fields.get('leechers', {}) + leechers = torrent(selector.get('selector', '')) + items = [item.text() for item in leechers.items() if item] + self.torrents_info['peers'] = items[0] if items else 0 + if 'filters' in selector: + self.torrents_info['peers'] = self.__filter_text(self.torrents_info.get('peers'), + selector.get('filters')) + + def Getseeders(self, torrent): + # torrent leechers + if 'seeders' not in self.fields: + return + selector = self.fields.get('seeders', {}) + seeders = torrent(selector.get('selector', '')) + items = [item.text() for item in seeders.items() if item] + self.torrents_info['seeders'] = items[0].split("/")[0] if items else 0 + if 'filters' in selector: + self.torrents_info['seeders'] = self.__filter_text(self.torrents_info.get('seeders'), + selector.get('filters')) + + def Getgrabs(self, torrent): + # torrent grabs + if 'grabs' not in self.fields: + return + selector = self.fields.get('grabs', {}) + grabs = torrent(selector.get('selector', '')) + items = [item.text() for item in grabs.items() if item] + self.torrents_info['grabs'] = items[0] if items else '' + if 'filters' in selector: + self.torrents_info['grabs'] = self.__filter_text(self.torrents_info.get('grabs'), + selector.get('filters')) + + def Getpubdate(self, torrent): + # torrent pubdate + if 'date_added' not in self.fields: + return + selector = self.fields.get('date_added', {}) + pubdate = torrent(selector.get('selector', '')) + if 'attribute' in selector: + items = [item.attr(selector.get('attribute')) for item in pubdate.items() if item] + else: + items = [item.text() for item in pubdate.items() if item] + self.torrents_info['pubdate'] = items[0] if items else '' + if 'filters' in selector: + self.torrents_info['pubdate'] = self.__filter_text(self.torrents_info.get('pubdate'), + selector.get('filters')) + + def Getelapsed_date(self, torrent): + # torrent pubdate + if 'date_elapsed' not in self.fields: + return + selector = self.fields.get('date_elapsed', {}) + date_elapsed = torrent(selector.get('selector', '')) + if 'attribute' in selector: + items = [item.attr(selector.get('attribute')) for item in date_elapsed.items() if item] + else: + items = [item.text() for item in date_elapsed.items() if item] + self.torrents_info['date_elapsed'] = items[0] if items else '' + if 'filters' in selector: + self.torrents_info['date_elapsed'] = self.__filter_text(self.torrents_info.get('date_elapsed'), + selector.get('filters')) + + def Getdownloadvolumefactor(self, torrent): + # downloadvolumefactor + selector = self.fields.get('downloadvolumefactor', {}) + if not selector: + return + if 'case' in selector: + for downloadvolumefactorselector in list(selector.get('case', + {}).keys()): + downloadvolumefactor = torrent(downloadvolumefactorselector) + if len(downloadvolumefactor) > 0: + self.torrents_info['downloadvolumefactor'] = selector.get('case', + {}).get( + downloadvolumefactorselector) + break + elif "selector" in selector: + downloadvolume = torrent(selector.get('selector', '')) + if downloadvolume: + items = [item.text() for item in downloadvolume.items() if item] + if items: + downloadvolumefactor = re.search(r'(\d+\.?\d*)', items[0]) + if downloadvolumefactor: + self.torrents_info['downloadvolumefactor'] = int(downloadvolumefactor.group(1)) + + def Getuploadvolumefactor(self, torrent): + # uploadvolumefactor + selector = self.fields.get('uploadvolumefactor', {}) + if not selector: + return + if 'case' in selector: + for uploadvolumefactorselector in list(selector.get('case', + {}).keys()): + uploadvolumefactor = torrent(uploadvolumefactorselector) + if len(uploadvolumefactor) > 0: + self.torrents_info['uploadvolumefactor'] = selector.get('case', + {}).get(uploadvolumefactorselector) + break + elif "selector" in selector: + uploadvolume = torrent(selector.get('selector', '')) + if uploadvolume: + items = [item.text() for item in uploadvolume.items() if item] + if items: + uploadvolumefactor = re.search(r'(\d+\.?\d*)', items[0]) + if uploadvolumefactor: + self.torrents_info['uploadvolumefactor'] = int(uploadvolumefactor.group(1)) + + def Getinfo(self, torrent): + """ + 解析单条种子数据 + """ + self.torrents_info = {'indexer': self.indexerid} + try: + self.Gettitle_default(torrent) + self.Gettitle_optional(torrent) + self.Getdetails(torrent) + self.Getdownload(torrent) + self.Getgrabs(torrent) + self.Getleechers(torrent) + self.Getseeders(torrent) + self.Getsize(torrent) + self.Getimdbid(torrent) + self.Getdownloadvolumefactor(torrent) + self.Getuploadvolumefactor(torrent) + self.Getpubdate(torrent) + self.Getelapsed_date(torrent) + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【Spider】%s 检索出现错误:%s" % (self.indexername, str(err))) + return self.torrents_info + + @staticmethod + def __filter_text(text, filters): + """ + 对文件进行处理 + """ + if not text or not filters or not isinstance(filters, list): + return text + if not isinstance(text, str): + text = str(text) + for filter_item in filters: + try: + method_name = filter_item.get("name") + args = filter_item.get("args") + if method_name == "re_search" and isinstance(args, list): + text = re.search(r"%s" % args[0], text).group(args[-1]) + elif method_name == "split" and isinstance(args, list): + text = text.split(r"%s" % args[0])[args[-1]] + elif method_name == "replace" and isinstance(args, list): + text = text.replace(r"%s" % args[0], r"%s" % args[-1]) + elif method_name == "dateparse" and isinstance(args, str): + text = datetime.datetime.strptime(text, r"%s" % args) + elif method_name == "strip": + text = text.strip() + elif method_name == "appendleft": + text = f"{args}{text}" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return text.strip() + + def parse(self, request, response): + """ + 解析整个页面 + """ + try: + # 获取站点文本 + html_text = response.extract() + if not html_text: + self.is_complete = True + return + # 解析站点文本对象 + html_doc = PyQuery(html_text) + # 种子筛选器 + torrents_selector = self.list.get('selector', '') + str_list = list(torrents_selector) + # 兼容选择器中has()函数 部分情况下无双引号会报错 + has_index = torrents_selector.find('has') + if has_index != -1 and torrents_selector.find('"') == -1: + flag = 0 + str_list.insert(has_index + 4, '"') + for i in range(len(str_list)): + if i > has_index + 2: + n = str_list[i] + if n == '(': + flag = flag + 1 + if n == ')': + flag = flag - 1 + if flag == 0: + str_list.insert(i, '"') + torrents_selector = "".join(str_list) + # 遍历种子html列表 + for torn in html_doc(torrents_selector): + self.torrents_info_array.append(copy.deepcopy(self.Getinfo(PyQuery(torn)))) + if len(self.torrents_info_array) >= int(self.result_num): + break + + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.warn("【Spider】错误:%s" % str(err)) + finally: + self.is_complete = True diff --git a/app/indexer/client/_tnode.py b/app/indexer/client/_tnode.py new file mode 100644 index 0000000..07727ff --- /dev/null +++ b/app/indexer/client/_tnode.py @@ -0,0 +1,104 @@ +import re + +import log +from app.utils import RequestUtils, StringUtils +from config import Config + + +class TNodeSpider(object): + _indexerid = None + _domain = None + _name = "" + _proxy = None + _cookie = None + _ua = None + _token = None + _size = 100 + _searchurl = "%sapi/torrent/advancedSearch" + _downloadurl = "%sapi/torrent/download/%s" + _pageurl = "%storrent/info/%s" + + def __init__(self, indexer): + if indexer: + self._indexerid = indexer.id + self._domain = indexer.domain + self._searchurl = self._searchurl % self._domain + self._name = indexer.name + if indexer.proxy: + self._proxy = Config().get_proxies() + self._cookie = indexer.cookie + self._ua = indexer.ua + self.init_config() + + def init_config(self): + self._size = Config().get_config('pt').get('site_search_result_num') or 100 + self.__get_token() + + def __get_token(self): + if not self._domain: + return + res = RequestUtils(headers=self._ua, + cookies=self._cookie, + proxies=self._proxy, + timeout=15).get_res(url=self._domain) + if res and res.status_code == 200: + csrf_token = re.search(r'', res.text) + if csrf_token: + self._token = csrf_token.group(1) + + def search(self, keyword, page=0): + if not self._token: + log.warn(f"【INDEXER】{self._name} 未获取到token,无法搜索") + return [] + params = { + "page": int(page) + 1, + "size": self._size, + "type": "title", + "keyword": keyword or "", + "sorter": "id", + "order": "desc", + "tags": [], + "category": [501, 502, 503, 504], + "medium": [], + "videoCoding": [], + "audioCoding": [], + "resolution": [], + "group": [] + } + res = RequestUtils( + headers={ + 'X-CSRF-TOKEN': self._token, + "Content-Type": "application/json; charset=utf-8", + "User-Agent": f"{self._ua}" + }, + cookies=self._cookie, + proxies=self._proxy, + timeout=30 + ).post_res(url=self._searchurl, json=params) + torrents = [] + if res and res.status_code == 200: + results = res.json().get('data', {}).get("torrents") or [] + for result in results: + torrent = { + 'indexer': self._indexerid, + 'title': result.get('title'), + 'description': result.get('subtitle'), + 'enclosure': self._downloadurl % (self._domain, result.get('id')), + 'pubdate': StringUtils.timestamp_to_date(result.get('upload_time')), + 'size': result.get('size'), + 'seeders': result.get('seeding'), + 'peers': result.get('leeching'), + 'grabs': result.get('complete'), + 'downloadvolumefactor': result.get('downloadRate'), + 'uploadvolumefactor': result.get('uploadRate'), + 'page_url': self._pageurl % (self._domain, result.get('id')), + 'imdbid': result.get('imdb') + } + torrents.append(torrent) + elif res is not None: + log.warn(f"【INDEXER】{self._name} 搜索失败,错误码:{res.status_code}") + return [] + else: + log.warn(f"【INDEXER】{self._name} 搜索失败,无法连接 {self._domain}") + return [] + return torrents diff --git a/app/indexer/client/builtin.py b/app/indexer/client/builtin.py new file mode 100644 index 0000000..318220a --- /dev/null +++ b/app/indexer/client/builtin.py @@ -0,0 +1,220 @@ +import copy +import datetime +import time + +import log +from app.helper import IndexerHelper, IndexerConf, ProgressHelper, ChromeHelper +from app.indexer.client._base import _IIndexClient +from app.indexer.client._rarbg import Rarbg +from app.indexer.client._render_spider import RenderSpider +from app.indexer.client._spider import TorrentSpider +from app.indexer.client._tnode import TNodeSpider +from app.sites import Sites +from app.utils import StringUtils +from app.utils.types import SearchType, IndexerType +from config import Config + + +class BuiltinIndexer(_IIndexClient): + schema = "builtin" + _client_config = {} + index_type = IndexerType.BUILTIN.value + progress = None + sites = None + + def __init__(self, config=None): + super().__init__() + self._client_config = config or {} + self.init_config() + + def init_config(self): + self.sites = Sites() + self.progress = ProgressHelper() + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.index_type] else False + + def get_status(self): + """ + 检查连通性 + :return: True、False + """ + return True + + def get_indexers(self, check=True, public=False, indexer_id=None): + ret_indexers = [] + # 选中站点配置 + indexer_sites = Config().get_config("pt").get("indexer_sites") or [] + _indexer_domains = [] + # 检查浏览器状态 + chrome_ok = ChromeHelper().get_status() + # 私有站点 + for site in Sites().get_sites(): + if not site.get("rssurl") and not site.get("signurl"): + continue + if not site.get("cookie"): + continue + url = site.get("signurl") or site.get("rssurl") + public_site = self.sites.get_public_sites(url=url) + if public_site: + if not public: + continue + is_public = True + proxy = public_site.get("proxy") + language = public_site.get("language") + render = False if not chrome_ok else public_site.get("render") + parser = public_site.get("parser") + else: + is_public = False + proxy = site.get("proxy") + language = None + render = False if not chrome_ok else None + parser = None + indexer = IndexerHelper().get_indexer(url=url, + cookie=site.get("cookie"), + ua=site.get("ua"), + name=site.get("name"), + rule=site.get("rule"), + pri=site.get('pri'), + public=is_public, + proxy=proxy, + render=render, + language=language, + parser=parser) + if indexer: + if indexer_id and indexer.id == indexer_id: + return indexer + if check and indexer_sites and indexer.id not in indexer_sites: + continue + if indexer.domain not in _indexer_domains: + _indexer_domains.append(indexer.domain) + indexer.name = site.get("name") + ret_indexers.append(indexer) + # 公开站点 + if public: + for site, attr in self.sites.get_public_sites(): + indexer = IndexerHelper().get_indexer(url=site, + public=True, + proxy=attr.get("proxy"), + render=attr.get("render"), + language=attr.get("language"), + parser=attr.get("parser")) + if indexer: + if indexer_id and indexer.id == indexer_id: + return indexer + if check and indexer_sites and indexer.id not in indexer_sites: + continue + if indexer.domain not in _indexer_domains: + _indexer_domains.append(indexer.domain) + ret_indexers.append(indexer) + return ret_indexers + + def search(self, order_seq, + indexer, + key_word, + filter_args: dict, + match_media, + in_from: SearchType): + """ + 根据关键字多线程检索 + """ + if not indexer or not key_word: + return None + # 不是配置的索引站点过滤掉 + indexer_sites = Config().get_config("pt").get("indexer_sites") or [] + if indexer_sites and indexer.id not in indexer_sites: + return [] + # fix 共用同一个dict时会导致某个站点的更新全局全效 + if filter_args is None: + _filter_args = {} + else: + _filter_args = copy.deepcopy(filter_args) + # 不在设定搜索范围的站点过滤掉 + if _filter_args.get("site") and indexer.name not in _filter_args.get("site"): + return [] + # 搜索条件没有过滤规则时,使用站点的过滤规则 + if not _filter_args.get("rule") and indexer.rule: + _filter_args.update({"rule": indexer.rule}) + # 计算耗时 + start_time = datetime.datetime.now() + log.info(f"【{self.index_type}】开始检索Indexer:{indexer.name} ...") + # 特殊符号处理 + search_word = StringUtils.handler_special_chars(text=key_word, + replace_word=" ", + allow_space=True) + # 避免对英文站搜索中文 + if indexer.language == "en" and StringUtils.is_chinese(search_word): + log.warn(f"【{self.index_type}】{indexer.name} 无法使用中文名搜索") + return [] + result_array = [] + try: + if indexer.parser == "Rarbg": + imdb_id = match_media.imdb_id if match_media else None + result_array = Rarbg().search(keyword=search_word, indexer=indexer, imdb_id=imdb_id) + elif indexer.parser == "TNodeSpider": + result_array = TNodeSpider(indexer=indexer).search(keyword=search_word) + elif indexer.parser == "RenderSpider": + result_array = RenderSpider().search(keyword=search_word, + indexer=indexer, + mtype=match_media.type if match_media else None) + else: + result_array = self.__spider_search(keyword=search_word, + indexer=indexer, + mtype=match_media.type if match_media else None) + except Exception as err: + print(str(err)) + if len(result_array) == 0: + log.warn(f"【{self.index_type}】{indexer.name} 未检索到数据") + self.progress.update(ptype='search', text=f"{indexer.name} 未检索到数据") + return [] + else: + log.warn(f"【{self.index_type}】{indexer.name} 返回数据:{len(result_array)}") + return self.filter_search_results(result_array=result_array, + order_seq=order_seq, + indexer=indexer, + filter_args=_filter_args, + match_media=match_media, + start_time=start_time) + + def list(self, index_id, page=0, keyword=None): + """ + 根据站点ID检索站点首页资源 + """ + if not index_id: + return [] + indexer: IndexerConf = self.get_indexers(indexer_id=index_id) + if not indexer: + return [] + if indexer.parser == "RenderSpider": + return RenderSpider().search(keyword=keyword, + indexer=indexer, + page=page) + elif indexer.parser == "TNodeSpider": + return TNodeSpider(indexer=indexer).search(keyword=keyword, page=page) + return self.__spider_search(indexer=indexer, + page=page, + keyword=keyword) + + @staticmethod + def __spider_search(indexer, keyword=None, page=None, mtype=None, timeout=30): + """ + 根据关键字搜索单个站点 + """ + spider = TorrentSpider() + spider.setparam(indexer=indexer, + keyword=keyword, + page=page, + mtype=mtype) + spider.start() + # 循环判断是否获取到数据 + sleep_count = 0 + while not spider.is_complete: + sleep_count += 1 + time.sleep(1) + if sleep_count > timeout: + break + # 返回数据 + result_array = spider.torrents_info_array.copy() + spider.torrents_info_array.clear() + return result_array diff --git a/app/indexer/indexer.py b/app/indexer/indexer.py new file mode 100644 index 0000000..ca07664 --- /dev/null +++ b/app/indexer/indexer.py @@ -0,0 +1,174 @@ +import datetime +from concurrent.futures import ThreadPoolExecutor, as_completed + +import log +from app.conf import ModuleConf +from app.helper import ProgressHelper, SubmoduleHelper +from app.indexer.client import BuiltinIndexer +from app.utils import ExceptionUtils, StringUtils +from app.utils.commons import singleton +from app.utils.types import SearchType, IndexerType +from config import Config + + +@singleton +class Indexer(object): + _indexer_schemas = [] + _client = None + _client_type = None + progress = None + + def __init__(self): + self._indexer_schemas = SubmoduleHelper.import_submodules( + 'app.indexer.client', + filter_func=lambda _, obj: hasattr(obj, 'schema') + ) + log.debug(f"【Indexer】加载索引器:{self._indexer_schemas}") + self.init_config() + + def init_config(self): + self.progress = ProgressHelper() + self._client_type = ModuleConf.INDEXER_DICT.get( + Config().get_config("pt").get('search_indexer') or 'builtin' + ) or IndexerType.BUILTIN + self._client = self.__get_client(self._client_type) + + def __build_class(self, ctype, conf): + for indexer_schema in self._indexer_schemas: + try: + if indexer_schema.match(ctype): + return indexer_schema(conf) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + def get_indexers(self): + """ + 获取当前索引器的索引站点 + """ + if not self._client: + return [] + return self._client.get_indexers() + + def get_indexer_dict(self): + """ + 获取索引器字典 + """ + return [ + { + "id": index.id, + "name": index.name + } for index in self.get_indexers() + ] + + def get_indexer_hash_dict(self): + """ + 获取索引器Hash字典 + """ + IndexerDict = {} + for item in self.get_indexers() or []: + IndexerDict[StringUtils.md5_hash(item.name)] = { + "id": item.id, + "name": item.name, + "public": item.public, + "builtin": item.builtin + } + return IndexerDict + + def get_indexer_names(self): + """ + 获取当前索引器的索引站点名称 + """ + return [indexer.name for indexer in self.get_indexers()] + + @staticmethod + def get_builtin_indexers(check=True, public=True, indexer_id=None): + """ + 获取内置索引器的索引站点 + """ + return BuiltinIndexer().get_indexers(check=check, public=public, indexer_id=indexer_id) + + @staticmethod + def list_builtin_resources(index_id, page=0, keyword=None): + """ + 获取内置索引器的资源列表 + :param index_id: 内置站点ID + :param page: 页码 + :param keyword: 搜索关键字 + """ + return BuiltinIndexer().list(index_id=index_id, page=page, keyword=keyword) + + def __get_client(self, ctype: IndexerType, conf=None): + return self.__build_class(ctype=ctype.value, conf=conf) + + def get_client(self): + """ + 获取当前索引器 + """ + return self._client + + def get_client_type(self): + """ + 获取当前索引器类型 + """ + return self._client_type + + def search_by_keyword(self, + key_word: [str, list], + filter_args: dict, + match_media=None, + in_from: SearchType = None): + """ + 根据关键字调用 Index API 检索 + :param key_word: 检索的关键字,不能为空 + :param filter_args: 过滤条件,对应属性为空则不过滤,{"season":季, "episode":集, "year":年, "type":类型, "site":站点, + "":, "restype":质量, "pix":分辨率, "sp_state":促销状态, "key":其它关键字} + sp_state: 为UL DL,* 代表不关心, + :param match_media: 需要匹配的媒体信息 + :param in_from: 搜索渠道 + :return: 命中的资源媒体信息列表 + """ + if not key_word: + return [] + + indexers = self.get_indexers() + if not indexers: + log.error(f"【{self._client_type.value}】没有有效的索引器配置!") + return [] + # 计算耗时 + start_time = datetime.datetime.now() + if filter_args and filter_args.get("site"): + log.info(f"【{self._client_type.value}】开始检索 %s,站点:%s ..." % (key_word, filter_args.get("site"))) + self.progress.update(ptype='search', text="开始检索 %s,站点:%s ..." % (key_word, filter_args.get("site"))) + else: + log.info(f"【{self._client_type.value}】开始并行检索 %s,线程数:%s ..." % (key_word, len(indexers))) + self.progress.update(ptype='search', text="开始并行检索 %s,线程数:%s ..." % (key_word, len(indexers))) + # 多线程 + executor = ThreadPoolExecutor(max_workers=len(indexers)) + all_task = [] + for index in indexers: + order_seq = 100 - int(index.pri) + task = executor.submit(self._client.search, + order_seq, + index, + key_word, + filter_args, + match_media, + in_from) + all_task.append(task) + ret_array = [] + finish_count = 0 + for future in as_completed(all_task): + result = future.result() + finish_count += 1 + self.progress.update(ptype='search', value=round(100 * (finish_count / len(all_task)))) + if result: + ret_array = ret_array + result + # 计算耗时 + end_time = datetime.datetime.now() + log.info(f"【{self._client_type.value}】所有站点检索完成,有效资源数:%s,总耗时 %s 秒" + % (len(ret_array), (end_time - start_time).seconds)) + self.progress.update(ptype='search', text="所有站点检索完成,有效资源数:%s,总耗时 %s 秒" + % (len(ret_array), (end_time - start_time).seconds), + value=100) + return ret_array diff --git a/app/media/__init__.py b/app/media/__init__.py new file mode 100644 index 0000000..2900436 --- /dev/null +++ b/app/media/__init__.py @@ -0,0 +1,5 @@ +from .category import Category +from .media import Media +from .scraper import Scraper +from .douban import DouBan +from .bangumi import Bangumi diff --git a/app/media/bangumi.py b/app/media/bangumi.py new file mode 100644 index 0000000..9d46a99 --- /dev/null +++ b/app/media/bangumi.py @@ -0,0 +1,105 @@ +from datetime import datetime +from functools import lru_cache + +import requests + +from app.utils import RequestUtils +from app.utils.types import MediaType + + +class Bangumi(object): + """ + https://bangumi.github.io/api/ + """ + + _urls = { + "calendar": "calendar", + "detail": "v0/subjects/%s", + } + _base_url = "https://api.bgm.tv/" + _req = RequestUtils(session=requests.Session()) + _page_num = 30 + + def __init__(self): + pass + + @classmethod + @lru_cache(maxsize=128) + def __invoke(cls, url, **kwargs): + req_url = cls._base_url + url + params = {} + if kwargs: + params.update(kwargs) + resp = cls._req.get_res(url=req_url, params=params) + return resp.json() if resp else None + + def calendar(self): + """ + 获取每日放送 + """ + return self.__invoke(self._urls["calendar"], _ts=datetime.strftime(datetime.now(), '%Y%m%d')) + + def detail(self, bid): + """ + 获取番剧详情 + """ + return self.__invoke(self._urls["detail"] % bid, _ts=datetime.strftime(datetime.now(), '%Y%m%d')) + + @staticmethod + def __dict_item(item, weekday): + """ + 转换为字典 + """ + bid = item.get("id") + detail = item.get("url") + title = item.get("name_cn", item.get("name")) + air_date = item.get("air_date") + rating = item.get("rating") + if rating: + score = rating.get("score") + else: + score = 0 + images = item.get("images") + if images: + image = images.get("large") + else: + image = '' + summary = item.get("summary") + return { + 'id': "BG:%s" % bid, + 'orgid': bid, + 'title': title, + 'year': air_date[:4] if air_date else "", + 'type': 'TV', + 'media_type': MediaType.TV.value, + 'vote': score, + 'image': image, + 'overview': summary, + 'url': detail, + 'weekday': weekday + } + + def get_bangumi_calendar(self, page=1, week=None): + """ + 获取每日放送 + """ + infos = self.calendar() + if not infos: + return [] + start_pos = (int(page) - 1) * self._page_num + ret_list = [] + pos = 0 + for info in infos: + weeknum = info.get("weekday", {}).get("id") + if week and int(weeknum) != int(week): + continue + weekday = info.get("weekday", {}).get("cn") + items = info.get("items") + for item in items: + if pos >= start_pos: + ret_list.append(self.__dict_item(item, weekday)) + pos += 1 + if pos >= start_pos + self._page_num: + break + + return ret_list diff --git a/app/media/category.py b/app/media/category.py new file mode 100644 index 0000000..11605df --- /dev/null +++ b/app/media/category.py @@ -0,0 +1,165 @@ +import os +import shutil + +import ruamel.yaml + +import log +from app.utils import ExceptionUtils +from config import Config +from app.utils.commons import singleton + + +@singleton +class Category: + _category_path = None + _categorys = None + _tv_categorys = None + _movie_categorys = None + _anime_categorys = None + + def __init__(self): + self.init_config() + + def init_config(self): + media = Config().get_config('media') + if media: + category = media.get('category') + if not category: + return + self._category_path = os.path.join(Config().get_config_path(), "%s.yaml" % category) + try: + if not os.path.exists(self._category_path): + shutil.copy(os.path.join(Config().get_inner_config_path(), "default-category.yaml"), + self._category_path) + log.console("【Config】分类配置文件 %s.yaml 不存在,已将配置文件模板复制到配置目录..." % category) + with open(self._category_path, mode='r', encoding='utf-8') as f: + try: + yaml = ruamel.yaml.YAML() + self._categorys = yaml.load(f) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.console("【Config】%s.yaml 分类配置文件格式出现严重错误!请检查:%s" % (category, str(e))) + self._categorys = {} + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.console("【Config】加载 %s.yaml 配置出错:%s" % (category, str(err))) + return False + + if self._categorys: + self._movie_categorys = self._categorys.get('movie') + self._tv_categorys = self._categorys.get('tv') + self._anime_categorys = self._categorys.get('anime') + + def get_movie_category_flag(self): + """ + 获取电影分类标志 + """ + if self._movie_categorys: + return True + return False + + def get_tv_category_flag(self): + """ + 获取电视剧分类标志 + """ + if self._tv_categorys: + return True + return False + + def get_anime_category_flag(self): + """ + 获取动漫分类标志 + """ + if self._anime_categorys: + return True + return False + + def get_movie_categorys(self): + """ + 获取电影分类清单 + """ + if not self._movie_categorys: + return [] + return self._movie_categorys.keys() + + def get_tv_categorys(self): + """ + 获取电视剧分类清单 + """ + if not self._tv_categorys: + return [] + return self._tv_categorys.keys() + + def get_anime_categorys(self): + """ + 获取动漫分类清单 + """ + if not self._anime_categorys: + return [] + return self._anime_categorys.keys() + + def get_movie_category(self, tmdb_info): + """ + 判断电影的分类 + :param tmdb_info: 识别的TMDB中的信息 + :return: 二级分类的名称 + """ + return self.get_category(self._movie_categorys, tmdb_info) + + def get_tv_category(self, tmdb_info): + """ + 判断电视剧的分类 + :param tmdb_info: 识别的TMDB中的信息 + :return: 二级分类的名称 + """ + return self.get_category(self._tv_categorys, tmdb_info) + + def get_anime_category(self, tmdb_info): + """ + 判断动漫的分类 + :param tmdb_info: 识别的TMDB中的信息 + :return: 二级分类的名称 + """ + return self.get_category(self._anime_categorys, tmdb_info) + + @staticmethod + def get_category(categorys, tmdb_info): + """ + 根据 TMDB信息与分类配置文件进行比较,确定所属分类 + :param categorys: 分类配置 + :param tmdb_info: TMDB信息 + :return: 分类的名称 + """ + if not tmdb_info: + return "" + if not categorys: + return "" + for key, item in categorys.items(): + if not item: + return key + match_flag = True + for attr, value in item.items(): + if not value: + continue + info_value = tmdb_info.get(attr) + if not info_value: + match_flag = False + continue + elif attr == "production_countries": + info_values = [str(val.get("iso_3166_1")).upper() for val in info_value] + else: + if isinstance(info_value, list): + info_values = [str(val).upper() for val in info_value] + else: + info_values = [str(info_value).upper()] + + if value.find(",") != -1: + values = [str(val).upper() for val in value.split(",")] + else: + values = [str(value).upper()] + + if not set(values).intersection(set(info_values)): + match_flag = False + if match_flag: + return key + return "" diff --git a/app/media/douban.py b/app/media/douban.py new file mode 100644 index 0000000..cb64f22 --- /dev/null +++ b/app/media/douban.py @@ -0,0 +1,442 @@ +import random +from threading import Lock +from time import sleep + +import zhconv + +from app.utils.commons import singleton +from app.utils import ExceptionUtils, StringUtils + +import log +from config import Config +from app.media.doubanapi import DoubanApi, DoubanWeb +from app.media.meta import MetaInfo +from app.utils import RequestUtils +from app.utils.types import MediaType + +lock = Lock() + + +@singleton +class DouBan: + cookie = None + doubanapi = None + doubanweb = None + message = None + _movie_num = 20 + _tv_num = 20 + + def __init__(self): + self.init_config() + + def init_config(self): + self.doubanapi = DoubanApi() + self.doubanweb = DoubanWeb() + douban = Config().get_config('douban') + if douban: + # Cookie + self.cookie = douban.get('cookie') + if not self.cookie: + try: + res = RequestUtils(timeout=5).get_res("https://www.douban.com/") + if res: + self.cookie = StringUtils.str_from_cookiejar(res.cookies) + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.warn(f"【Douban】获取cookie失败:{format(err)}") + + def get_douban_detail(self, doubanid, mtype=None, wait=False): + """ + 根据豆瓣ID返回豆瓣详情,带休眠 + """ + log.info("【Douban】正在通过API查询豆瓣详情:%s" % doubanid) + # 随机休眠 + if wait: + time = round(random.uniform(1, 5), 1) + log.info("【Douban】随机休眠:%s 秒" % time) + sleep(time) + if mtype == MediaType.MOVIE: + douban_info = self.doubanapi.movie_detail(doubanid) + elif mtype: + douban_info = self.doubanapi.tv_detail(doubanid) + else: + douban_info = self.doubanapi.movie_detail(doubanid) + if not douban_info: + douban_info = self.doubanapi.tv_detail(doubanid) + if not douban_info: + log.warn("【Douban】%s 未找到豆瓣详细信息" % doubanid) + return None + if douban_info.get("localized_message"): + log.warn("【Douban】查询豆瓣详情错误:%s" % douban_info.get("localized_message")) + return None + if not douban_info.get("title"): + return None + if douban_info.get("title") == "未知电影" or douban_info.get("title") == "未知电视剧": + return None + log.info("【Douban】查询到数据:%s" % douban_info.get("title")) + return douban_info + + def __search_douban_id(self, metainfo): + """ + 给定名称和年份,查询一条豆瓣信息返回对应ID + :param metainfo: 已进行识别过的媒体信息 + """ + if metainfo.year: + year_range = [int(metainfo.year), int(metainfo.year) + 1, int(metainfo.year) - 1] + else: + year_range = [] + if metainfo.type == MediaType.MOVIE: + search_res = self.doubanapi.movie_search(metainfo.title).get("items") or [] + if not search_res: + return None + for res in search_res: + douban_meta = MetaInfo(title=res.get("target", {}).get("title")) + if metainfo.title == douban_meta.get_name() \ + and (int(res.get("target", {}).get("year")) in year_range or not year_range): + return res.get("target_id") + return None + elif metainfo.type == MediaType.TV: + search_res = self.doubanapi.tv_search(metainfo.title).get("items") or [] + if not search_res: + return None + for res in search_res: + douban_meta = MetaInfo(title=res.get("target", {}).get("title")) + if metainfo.title == douban_meta.get_name() \ + and (str(res.get("target", {}).get("year")) == str(metainfo.year) or not metainfo.year): + return res.get("target_id") + if metainfo.title == douban_meta.get_name() \ + and metainfo.get_season_string() == douban_meta.get_season_string(): + return res.get("target_id") + return search_res[0].get("target_id") + + def get_douban_info(self, metainfo): + """ + 查询附带演职人员的豆瓣信息 + :param metainfo: 已进行识别过的媒体信息 + """ + doubanid = self.__search_douban_id(metainfo) + if not doubanid: + return None + if metainfo.type == MediaType.MOVIE: + douban_info = self.doubanapi.movie_detail(doubanid) + celebrities = self.doubanapi.movie_celebrities(doubanid) + if douban_info and celebrities: + douban_info["directors"] = celebrities.get("directors") + douban_info["actors"] = celebrities.get("actors") + return douban_info + elif metainfo.type == MediaType.TV: + douban_info = self.doubanapi.tv_detail(doubanid) + celebrities = self.doubanapi.tv_celebrities(doubanid) + if douban_info and celebrities: + douban_info["directors"] = celebrities.get("directors") + douban_info["actors"] = celebrities.get("actors") + return douban_info + + def get_douban_wish(self, dtype, userid, start, wait=False): + """ + 获取豆瓣想看列表数据 + """ + if wait: + time = round(random.uniform(1, 5), 1) + log.info("【Douban】随机休眠:%s 秒" % time) + sleep(time) + if dtype == "do": + web_infos = self.doubanweb.do(cookie=self.cookie, userid=userid, start=start) + elif dtype == "collect": + web_infos = self.doubanweb.collect(cookie=self.cookie, userid=userid, start=start) + else: + web_infos = self.doubanweb.wish(cookie=self.cookie, userid=userid, start=start) + if not web_infos: + return [] + for web_info in web_infos: + web_info["id"] = web_info.get("url").split("/")[-2] + return web_infos + + def get_user_info(self, userid, wait=False): + if wait: + time = round(random.uniform(1, 5), 1) + log.info("【Douban】随机休眠:%s 秒" % time) + sleep(time) + return self.doubanweb.user(cookie=self.cookie, userid=userid) + + def search_douban_medias(self, keyword, mtype: MediaType = None, season=None, episode=None, page=1): + """ + 根据关键字搜索豆瓣,返回可能的标题和年份信息 + """ + if not keyword: + return [] + result = self.doubanapi.search(keyword) + if not result: + return [] + ret_medias = [] + for item_obj in result.get("items"): + if mtype and mtype.value != item_obj.get("type_name"): + continue + if item_obj.get("type_name") not in (MediaType.TV.value, MediaType.MOVIE.value): + continue + item = item_obj.get("target") + meta_info = MetaInfo(title=item.get("title")) + meta_info.title = item.get("title") + if item_obj.get("type_name") == MediaType.MOVIE.value: + meta_info.type = MediaType.MOVIE + else: + meta_info.type = MediaType.TV + if season: + if meta_info.type != MediaType.TV: + continue + if season != 1 and meta_info.begin_season != season: + continue + if episode and str(episode).isdigit(): + if meta_info.type != MediaType.TV: + continue + meta_info.begin_episode = int(episode) + meta_info.title = "%s 第%s集" % (meta_info.title, episode) + meta_info.year = item.get("year") + meta_info.tmdb_id = "DB:%s" % item.get("id") + meta_info.douban_id = item.get("id") + meta_info.overview = item.get("card_subtitle") or "" + meta_info.poster_path = item.get("cover_url").split('?')[0] + rating = item.get("rating", {}) or {} + meta_info.vote_average = rating.get("value") + if meta_info not in ret_medias: + ret_medias.append(meta_info) + + return ret_medias[(page - 1) * 20:page * 20] + + def get_media_detail_from_web(self, doubanid): + """ + 从豆瓣详情页抓紧媒体信息 + :param doubanid: 豆瓣ID + :return: {title, year, intro, cover_url, rating{value}, episodes_count} + """ + log.info("【Douban】正在通过网页查询豆瓣详情:%s" % doubanid) + web_info = self.doubanweb.detail(cookie=self.cookie, doubanid=doubanid) + if not web_info: + return {} + ret_media = {} + try: + # 标题 + title = web_info.get("title") + if title: + title = title + metainfo = MetaInfo(title=title) + if metainfo.cn_name: + title = metainfo.cn_name + # 有中文的去掉日文和韩文 + if title and StringUtils.is_chinese(title) and " " in title: + titles = title.split() + title = titles[0] + for _title in titles[1:]: + # 忽略繁体 + if zhconv.convert(_title, 'zh-hans') == title: + break + # 忽略日韩文 + if not StringUtils.is_japanese(_title) \ + and not StringUtils.is_korean(_title): + title = f"{title} {_title}" + break + else: + break + else: + title = metainfo.en_name + if not title: + return None + ret_media['title'] = title + ret_media['season'] = metainfo.begin_season + else: + return None + # 年份 + year = web_info.get("year") + if year: + ret_media['year'] = year[1:-1] + # 简介 + ret_media['intro'] = "".join( + [str(x).strip() for x in web_info.get("intro") or []]) + # 封面图 + cover_url = web_info.get("cover") + if cover_url: + ret_media['cover_url'] = cover_url.replace("s_ratio_poster", "m_ratio_poster") + # 评分 + rating = web_info.get("rate") + if rating: + ret_media['rating'] = {"value": float(rating)} + # 季数 + season_num = web_info.get("season_num") + if season_num: + ret_media['season'] = int(season_num) + # 集数 + episode_num = web_info.get("episode_num") + if episode_num: + ret_media['episodes_count'] = int(episode_num) + # IMDBID + imdbid = web_info.get('imdb') + if imdbid: + ret_media['imdbid'] = str(imdbid).strip() + except Exception as err: + ExceptionUtils.exception_traceback(err) + if ret_media: + log.info("【Douban】查询到数据:%s" % ret_media.get("title")) + else: + log.warn("【Douban】%s 未查询到豆瓣数据:%s" % doubanid) + return ret_media + + def get_douban_online_movie(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.movie_showing(start=(page - 1) * self._movie_num, + count=self._movie_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_hot_movie(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.movie_hot_gaia(start=(page - 1) * self._movie_num, + count=self._movie_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_hot_anime(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.tv_animation(start=(page - 1) * self._tv_num, + count=self._tv_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_hot_tv(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.tv_hot(start=(page - 1) * self._tv_num, + count=self._tv_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_new_movie(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.movie_soon(start=(page - 1) * self._movie_num, + count=self._movie_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_hot_show(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.show_hot(start=(page - 1) * self._tv_num, + count=self._tv_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_top250_movie(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.movie_top250(start=(page - 1) * self._movie_num, + count=self._movie_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_chinese_weekly_tv(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.tv_chinese_best_weekly(start=(page - 1) * self._tv_num, + count=self._tv_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_weekly_tv_global(self, page=1): + if not self.doubanapi: + return [] + infos = self.doubanapi.tv_global_best_weekly(start=(page - 1) * self._tv_num, + count=self._tv_num) + if not infos: + return [] + return self.__dict_items(infos.get("subject_collection_items")) + + def get_douban_disover(self, mtype, sort, tags, page=1): + if not self.doubanapi: + return [] + if mtype == MediaType.MOVIE: + infos = self.doubanapi.movie_recommend(start=(page - 1) * self._movie_num, + count=self._movie_num, + sort=sort, + tags=tags) + else: + infos = self.doubanapi.tv_recommend(start=(page - 1) * self._tv_num, + count=self._tv_num, + sort=sort, + tags=tags) + if not infos: + return [] + return self.__dict_items(infos.get("items")) + + @staticmethod + def __dict_items(infos, media_type=None): + """ + 转化为字典 + """ + # ID + ret_infos = [] + for info in infos: + rid = info.get("id") + # 评分 + rating = info.get('rating') + if rating: + vote_average = float(rating.get("value")) + else: + vote_average = 0 + # 标题 + title = info.get('title') + # 年份 + year = info.get('year') + + if not media_type: + if info.get("type") not in ("movie", "tv"): + continue + mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV + else: + mtype = media_type + + if mtype == MediaType.MOVIE: + type_str = "MOV" + # 海报 + poster_path = info.get('cover', {}).get("url") + if not poster_path: + poster_path = info.get('cover_url') + if not poster_path: + poster_path = info.get('pic', {}).get("large") + else: + type_str = "TV" + # 海报 + poster_path = info.get('pic', {}).get("normal") + + # 简介 + overview = info.get("card_subtitle") or "" + if not year and overview: + if overview.split("/")[0].strip().isdigit(): + year = overview.split("/")[0].strip() + + # 高清海报 + if poster_path: + poster_path = poster_path.replace("s_ratio_poster", "m_ratio_poster") + + ret_infos.append({ + 'id': "DB:%s" % rid, + 'orgid': rid, + 'title': title, + 'type': type_str, + 'media_type': mtype.value, + 'year': year[:4] if year else "", + 'vote': vote_average, + 'image': poster_path, + 'overview': overview + }) + return ret_infos diff --git a/app/media/doubanapi/__init__.py b/app/media/doubanapi/__init__.py new file mode 100644 index 0000000..81f6bce --- /dev/null +++ b/app/media/doubanapi/__init__.py @@ -0,0 +1,2 @@ +from .apiv2 import DoubanApi +from .webapi import DoubanWeb diff --git a/app/media/doubanapi/apiv2.py b/app/media/doubanapi/apiv2.py new file mode 100644 index 0000000..6a92413 --- /dev/null +++ b/app/media/doubanapi/apiv2.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +import base64 +import hashlib +import hmac +from datetime import datetime +from functools import lru_cache +from random import choice +from urllib import parse + +import requests + +from app.utils import RequestUtils +from app.utils.commons import singleton + + +@singleton +class DoubanApi(object): + _urls = { + # 搜索类 + # sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映 + # q=search_word&start=0&count=20&sort=U + # 聚合搜索 + "search": "/search/weixin", + "search_agg": "/search", + + # 电影探索 + # sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间 + # tags='日本,动画,2022'&start=0&count=20&sort=U + "movie_recommend": "/movie/recommend", + # 电视剧探索 + "tv_recommend": "/tv/recommend", + # 搜索 + "movie_tag": "/movie/tag", + "tv_tag": "/tv/tag", + # q=search_word&start=0&count=20 + "movie_search": "/search/movie", + "tv_search": "/search/movie", + "book_search": "/search/book", + "group_search": "/search/group", + + # 各类主题合集 + # start=0&count=20 + # 正在上映 + "movie_showing": "/subject_collection/movie_showing/items", + # 热门电影 + "movie_hot_gaia": "/subject_collection/movie_hot_gaia/items", + # 即将上映 + "movie_soon": "/subject_collection/movie_soon/items", + # TOP250 + "movie_top250": "/subject_collection/movie_top250/items", + # 高分经典科幻片榜 + "movie_scifi": "/subject_collection/movie_scifi/items", + # 高分经典喜剧片榜 + "movie_comedy": "/subject_collection/movie_comedy/items", + # 高分经典动作片榜 + "movie_action": "/subject_collection/movie_action/items", + # 高分经典爱情片榜 + "movie_love": "/subject_collection/movie_love/items", + + # 热门剧集 + "tv_hot": "/subject_collection/tv_hot/items", + # 国产剧 + "tv_domestic": "/subject_collection/tv_domestic/items", + # 美剧 + "tv_american": "/subject_collection/tv_american/items", + # 本剧 + "tv_japanese": "/subject_collection/tv_japanese/items", + # 韩剧 + "tv_korean": "/subject_collection/tv_korean/items", + # 动画 + "tv_animation": "/subject_collection/tv_animation/items", + # 综艺 + "tv_variety_show": "/subject_collection/tv_variety_show/items", + # 华语口碑周榜 + "tv_chinese_best_weekly": "/subject_collection/tv_chinese_best_weekly/items", + # 全球口碑周榜 + "tv_global_best_weekly": "/subject_collection/tv_global_best_weekly/items", + + # 执门综艺 + "show_hot": "/subject_collection/show_hot/items", + # 国内综艺 + "show_domestic": "/subject_collection/show_domestic/items", + # 国外综艺 + "show_foreign": "/subject_collection/show_foreign/items", + + "book_bestseller": "/subject_collection/book_bestseller/items", + "book_top250": "/subject_collection/book_top250/items", + # 虚构类热门榜 + "book_fiction_hot_weekly": "/subject_collection/book_fiction_hot_weekly/items", + # 非虚构类热门 + "book_nonfiction_hot_weekly": "/subject_collection/book_nonfiction_hot_weekly/items", + + # 音乐 + "music_single": "/subject_collection/music_single/items", + + # rank list + "movie_rank_list": "/movie/rank_list", + "movie_year_ranks": "/movie/year_ranks", + "book_rank_list": "/book/rank_list", + "tv_rank_list": "/tv/rank_list", + + # movie info + "movie_detail": "/movie/", + "movie_rating": "/movie/%s/rating", + "movie_photos": "/movie/%s/photos", + "movie_trailers": "/movie/%s/trailers", + "movie_interests": "/movie/%s/interests", + "movie_reviews": "/movie/%s/reviews", + "movie_recommendations": "/movie/%s/recommendations", + "movie_celebrities": "/movie/%s/celebrities", + + # tv info + "tv_detail": "/tv/", + "tv_rating": "/tv/%s/rating", + "tv_photos": "/tv/%s/photos", + "tv_trailers": "/tv/%s/trailers", + "tv_interests": "/tv/%s/interests", + "tv_reviews": "/tv/%s/reviews", + "tv_recommendations": "/tv/%s/recommendations", + "tv_celebrities": "/tv/%s/celebrities", + + # book info + "book_detail": "/book/", + "book_rating": "/book/%s/rating", + "book_interests": "/book/%s/interests", + "book_reviews": "/book/%s/reviews", + "book_recommendations": "/book/%s/recommendations", + + # music info + "music_detail": "/music/", + "music_rating": "/music/%s/rating", + "music_interests": "/music/%s/interests", + "music_reviews": "/music/%s/reviews", + "music_recommendations": "/music/%s/recommendations", + } + + _user_agents = [ + "api-client/1 com.douban.frodo/7.22.0.beta9(231) Android/23 product/Mate 40 vendor/HUAWEI model/Mate 40 brand/HUAWEI rom/android network/wifi platform/AndroidPad" + "api-client/1 com.douban.frodo/7.18.0(230) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1", + "api-client/1 com.douban.frodo/7.1.0(205) Android/29 product/perseus vendor/Xiaomi model/Mi MIX 3 rom/miui6 network/wifi platform/mobile nd/1", + "api-client/1 com.douban.frodo/7.3.0(207) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1"] + _api_secret_key = "bf7dddc7c9cfe6f7" + _api_key = "0dad551ec0f84ed02907ff5c42e8ec70" + _base_url = "https://frodo.douban.com/api/v2" + _session = requests.Session() + + def __init__(self): + pass + + @classmethod + def __sign(cls, url: str, ts: int, method='GET') -> str: + url_path = parse.urlparse(url).path + raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)]) + return base64.b64encode(hmac.new(cls._api_secret_key.encode(), raw_sign.encode(), hashlib.sha1).digest() + ).decode() + + @classmethod + @lru_cache(maxsize=256) + def __invoke(cls, url, **kwargs): + req_url = cls._base_url + url + + params = {'apiKey': cls._api_key} + if kwargs: + params.update(kwargs) + + ts = params.pop('_ts', int(datetime.strftime(datetime.now(), '%Y%m%d'))) + params.update({'os_rom': 'android', 'apiKey': cls._api_key, '_ts': ts, '_sig': cls.__sign(url=req_url, ts=ts)}) + + headers = {'User-Agent': choice(cls._user_agents)} + resp = RequestUtils(headers=headers, session=cls._session).get_res(url=req_url, params=params) + + return resp.json() if resp else None + + def search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["search"], q=keyword, start=start, count=count, _ts=ts) + + def movie_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_search"], q=keyword, start=start, count=count, _ts=ts) + + def tv_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_search"], q=keyword, start=start, count=count, _ts=ts) + + def book_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["book_search"], q=keyword, start=start, count=count, _ts=ts) + + def group_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["group_search"], q=keyword, start=start, count=count, _ts=ts) + + def movie_showing(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_showing"], start=start, count=count, _ts=ts) + + def movie_soon(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_soon"], start=start, count=count, _ts=ts) + + def movie_hot_gaia(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_hot_gaia"], start=start, count=count, _ts=ts) + + def tv_hot(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_hot"], start=start, count=count, _ts=ts) + + def tv_animation(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_animation"], start=start, count=count, _ts=ts) + + def tv_variety_show(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_variety_show"], start=start, count=count, _ts=ts) + + def tv_rank_list(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_rank_list"], start=start, count=count, _ts=ts) + + def show_hot(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["show_hot"], start=start, count=count, _ts=ts) + + def movie_detail(self, subject_id): + return self.__invoke(self._urls["movie_detail"] + subject_id) + + def movie_celebrities(self, subject_id): + return self.__invoke(self._urls["movie_celebrities"] % subject_id) + + def tv_detail(self, subject_id): + return self.__invoke(self._urls["tv_detail"] + subject_id) + + def tv_celebrities(self, subject_id): + return self.__invoke(self._urls["tv_celebrities"] % subject_id) + + def book_detail(self, subject_id): + return self.__invoke(self._urls["book_detail"] + subject_id) + + def movie_top250(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_top250"], start=start, count=count, _ts=ts) + + def movie_recommend(self, tags='', sort='T', start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort, start=start, count=count, _ts=ts) + + def tv_recommend(self, tags='', sort='T', start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort, start=start, count=count, _ts=ts) + + def tv_chinese_best_weekly(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_chinese_best_weekly"], start=start, count=count, _ts=ts) + + def tv_global_best_weekly(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')): + return self.__invoke(self._urls["tv_global_best_weekly"], start=start, count=count, _ts=ts) diff --git a/app/media/doubanapi/webapi.py b/app/media/doubanapi/webapi.py new file mode 100644 index 0000000..801f36f --- /dev/null +++ b/app/media/doubanapi/webapi.py @@ -0,0 +1,294 @@ +from functools import lru_cache + +import requests +from lxml import etree + +from app.utils import RequestUtils, ExceptionUtils +from app.utils.commons import singleton + + +@singleton +class DoubanWeb(object): + + _session = requests.Session() + + _movie_base = "https://movie.douban.com" + _search_base = "https://search.douban.com" + _page_limit = 50 + _timout = 5 + + _weburls = { + # 详情 + "detail": f"{_movie_base}/subject/%s", + # 正在热映 + "nowplaying": f"{_movie_base}/cinema/nowplaying", + # 即将上映 + "later": f"{_movie_base}/cinema/later", + # 看过 + "collect": f"{_movie_base}/people/%s/collect?start=%s&sort=time&rating=all&filter=all&mode=grid", + # 想看 + "wish": f"{_movie_base}/people/%s/wish?start=%s&sort=time&rating=all&filter=all&mode=grid", + # 在看 + "do": f"{_movie_base}/people/%s/do?start=%s&sort=time&rating=all&filter=all&mode=grid", + # 搜索 + "search": f"{_search_base}/movie/subject_search?search_text=%s", + # TOP 250 + "top250": f"{_movie_base}/top250", + # 用户名称 + "user": f"{_movie_base}/people/%s/", + } + + _webparsers = { + "detail": { + "title": "//span[@property='v:itemreviewed']/text()", + "year": "//div[@id='content']//span[@class='year']/text()", + "intro": "//span[@property='v:summary']/text()", + "cover": "//div[@id='mainpic']//img/@src", + "rate": "//strong[@property='v:average']/text()", + "imdb": "//div[@id='info']/span[contains(text(), 'IMDb:')]/following-sibling::text()", + "season": "//div[@id='info']/span[contains(text(), '季数')]/following-sibling::text()", + "episode_num": "//div[@id='info']/span[contains(text(), '集数')]/following-sibling::text()" + }, + "nowplaying": { + "list": "//div[@id='nowplaying']//ul[@class='lists']/li", + "item": { + "id": "./@data-subject", + "title": "./@data-title", + "rate": "./@data-score", + "cover": "./li[@class='poster']/a/img/@src", + "year": "./@data-release" + } + }, + "later": { + "list": "//div[@id='showing-soon']/div", + "item": { + "id": "./@data-subject", + "title": "./div[@class='intro']/h3/a/text()]", + "cover": "./a[class='thumb']/img/@src", + "url": "./div[@class='intro']/h3/a/@href" + } + }, + "top250": { + "list": "//ol[@class='grid_view']/li", + "dates": "//div[@class='info']//span[@class='date']/text()", + "item": { + "title": "./div[@class='item']/div[@class='pic']/a/img/@alt", + "cover": "./div[@class='item']/div[@class='pic']/a/img/@src", + "url": "./div[@class='item']/div[@class='pic']/a/@href" + } + }, + "collect": { + "list": "//div[@class='grid-view']/div[@class='item']", + "dates": "//div[@class='info']//span[@class='date']/text()", + "item": { + "title": "./div[@class='info']/ul/li[@class='title']/a/em/text()", + "cover": "./div[@class='pic']/a/img/@src", + "url": "./div[@class='info']/ul/li[@class='title']/a/@href" + } + }, + "wish": { + "list": "//div[@class='grid-view']/div[@class='item']", + "item": { + "title": "./div[@class='info']/ul/li[@class='title']/a/em/text()", + "cover": "./div[@class='pic']/a/img/@src", + "url": "./div[@class='info']/ul/li[@class='title']/a/@href", + "date": "./div[@class='info']//span[@class='date']/text()" + } + }, + "do": { + "list": "//div[@class='grid-view']/div[@class='item']", + "item": { + "title": "./div[@class='info']/ul/li[@class='title']/a/em/text()", + "cover": "./div[@class='pic']/a/img/@src", + "url": "./div[@class='info']/ul/li[@class='title']/a/@href" + } + }, + "search": { + "list": "//div[@class='item-root']", + "item": { + "title": "./div[@class='title']/a/text()", + "url": "./div[@class='detail']/div[@class='title']/a/@href", + "cover": "./a/img[class='cover']/@src", + "intro": "./div[@class='detail']/div[@class='meta abstract']/text()", + "rate": "./div[@class='detail']/div[@class='rating']/span[@class='rating_nums']/text()", + "actor": "./div[@class='detail']/div[@class='meta abstract_2']/text()" + } + }, + "user": { + "name": "//div[@class='side-info']/div[@class='side-info-txt']/h3/text()" + } + } + + _jsonurls = { + # 最新电影 + "movie_new": f"{_movie_base}/j/search_subjects?type=movie&tag=最新&page_limit={_page_limit}&page_start=%s", + # 热门电影 + "movie_hot": f"{_movie_base}/j/search_subjects?type=movie&tag=热门&page_limit={_page_limit}&page_start=%s", + # 高分电影 + "movie_rate": f"{_movie_base}/j/search_subjects?type=movie&tag=豆瓣高分&page_limit={_page_limit}&page_start=%s", + # 热门电视剧 + "tv_hot": f"{_movie_base}/j/search_subjects?type=tv&tag=热门&page_limit={_page_limit}&page_start=%s", + # 热门动漫 + "anime_hot": f"{_movie_base}/j/search_subjects?type=tv&tag=日本动画&page_limit={_page_limit}&page_start=%s", + # 热门综艺 + "variety_hot": f"{_movie_base}/j/search_subjects?type=tv&tag=综艺&page_limit={_page_limit}&page_start=%s", + } + + def __int__(self, cookie=None): + pass + + @classmethod + def __invoke_web(cls, url, cookie, *kwargs): + req_url = cls._weburls.get(url) + if not req_url: + return None + return RequestUtils(cookies=cookie, + session=cls._session, + timeout=cls._timout).get(url=req_url % kwargs) + + @classmethod + def __invoke_json(cls, url, *kwargs): + req_url = cls._jsonurls.get(url) + if not req_url: + return None + req = RequestUtils(session=cls._session, + timeout=cls._timout).get_res(url=req_url % kwargs) + return req.json() if req else None + + @staticmethod + def __get_json(json): + if not json: + return None + return json.get("subjects") + + @classmethod + def __get_list(cls, url, html): + if not url or not html: + return None + xpaths = cls._webparsers.get(url) + if not xpaths: + return None + items = etree.HTML(html).xpath(xpaths.get("list")) + if not items: + return None + result = [] + for item in items: + obj = {} + for key, value in xpaths.get("item").items(): + text = item.xpath(value) + if text: + obj[key] = text[0] + if obj: + result.append(obj) + return result + + @classmethod + def __get_obj(cls, url, html): + if not url or not html: + return None + xpaths = cls._webparsers.get(url) + if not xpaths: + return None + obj = {} + for key, value in xpaths.items(): + try: + text = etree.HTML(html).xpath(value) + if text: + obj[key] = text[0] + except Exception as e: + ExceptionUtils.exception_traceback(e) + return obj + + @classmethod + @lru_cache(maxsize=256) + def detail(cls, cookie, doubanid): + """ + 查询详情 + """ + return cls.__get_obj("detail", cls.__invoke_web("detail", cookie, doubanid)) + + @classmethod + @lru_cache(maxsize=10) + def user(cls, cookie, userid): + """ + 查询用户信息 + """ + return cls.__get_obj("user", cls.__invoke_web("user", cookie, userid)) + + def nowplaying(self, cookie): + """ + 正在热映 + """ + return self.__get_list("nowplaying", self.__invoke_web("nowplaying", cookie)) + + def later(self, cookie): + """ + 即将上映 + """ + return self.__get_list("later", self.__invoke_web("later", cookie)) + + def collect(self, cookie, userid, start=0): + """ + 看过 + """ + return self.__get_list("collect", self.__invoke_web("collect", cookie, userid, start)) + + def wish(self, cookie, userid, start=0): + """ + 想看 + """ + return self.__get_list("wish", self.__invoke_web("wish", cookie, userid, start)) + + def do(self, cookie, userid, start=0): + """ + 在看 + """ + return self.__get_list("do", self.__invoke_web("do", cookie, userid, start)) + + def search(self, cookie, keyword): + """ + 搜索 + """ + return self.__get_list("search", self.__invoke_web("search", cookie, keyword)) + + def top250(self, cookie): + """ + TOP 250 + """ + return self.__get_list("top250", self.__invoke_web("top250", cookie)) + + def movie_new(self, start=0): + """ + 最新电影 + """ + return self.__get_json(self.__invoke_json("movie_new", start)) + + def movie_hot(self, start=0): + """ + 热门电影 + """ + return self.__get_json(self.__invoke_json("movie_hot", start)) + + def movie_rate(self, start=0): + """ + 高分电影 + """ + return self.__get_json(self.__invoke_json("movie_rate", start)) + + def tv_hot(self, start=0): + """ + 热门电视剧 + """ + return self.__get_json(self.__invoke_json("tv_hot", start)) + + def anime_hot(self, start=0): + """ + 热门动漫 + """ + return self.__get_json(self.__invoke_json("anime_hot", start)) + + def variety_hot(self, start=0): + """ + 热门综艺 + """ + return self.__get_json(self.__invoke_json("variety_hot", start)) diff --git a/app/media/fanart.py b/app/media/fanart.py new file mode 100644 index 0000000..4dd53f8 --- /dev/null +++ b/app/media/fanart.py @@ -0,0 +1,214 @@ +from functools import lru_cache + +from app.utils import RequestUtils, ExceptionUtils +from app.utils.types import MediaType +from config import Config, FANART_MOVIE_API_URL, FANART_TV_API_URL + + +class Fanart: + _proxies = Config().get_proxies() + _movie_image_types = ['movieposter', + 'hdmovielogo', + 'moviebackground', + 'moviedisc', + 'moviebanner', + 'moviethumb'] + _tv_image_types = ['hdtvlogo', + 'tvthumb', + 'showbackground', + 'tvbanner', + 'seasonposter', + 'seasonbanner', + 'seasonthumb', + 'tvposter', + 'hdclearart'] + _images = {} + + def __init__(self): + self.init_config() + + def init_config(self): + self._images = {} + + def __get_fanart_images(self, media_type, queryid): + if not media_type or not queryid: + return + try: + ret = self.__request_fanart(media_type=media_type, queryid=queryid) + if ret and ret.status_code == 200: + if media_type == MediaType.MOVIE: + for image_type in self._movie_image_types: + images = ret.json().get(image_type) + if isinstance(images, list): + self._images[image_type] = images[0].get('url') if isinstance(images[0], dict) else "" + else: + self._images[image_type] = "" + else: + for image_type in self._tv_image_types: + images = ret.json().get(image_type) + if isinstance(images, list): + if image_type in ['seasonposter', 'seasonthumb', 'seasonbanner']: + if not self._images.get(image_type): + self._images[image_type] = {} + for image in images: + if image.get("season") not in self._images[image_type].keys(): + self._images[image_type][image.get("season")] = image.get("url") + else: + self._images[image_type] = images[0].get('url') if isinstance(images[0], dict) else "" + else: + self._images[image_type] = "" + except Exception as e2: + ExceptionUtils.exception_traceback(e2) + + @classmethod + @lru_cache(maxsize=256) + def __request_fanart(cls, media_type, queryid): + if media_type == MediaType.MOVIE: + image_url = FANART_MOVIE_API_URL % queryid + else: + image_url = FANART_TV_API_URL % queryid + try: + return RequestUtils(proxies=cls._proxies, timeout=5).get_res(image_url) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return None + + def get_backdrop(self, media_type, queryid, default=""): + """ + 获取横幅背景图 + """ + if not media_type or not queryid: + return "" + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("moviethumb", default) + else: + return self._images.get("tvthumb", default) + + def get_poster(self, media_type, queryid, default=None): + """ + 获取海报 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("movieposter", default) + else: + return self._images.get("tvposter", default) + + def get_background(self, media_type, queryid, default=None): + """ + 获取海报 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("moviebackground", default) + else: + return self._images.get("showbackground", default) + + def get_banner(self, media_type, queryid, default=None): + """ + 获取海报 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("moviebanner", default) + else: + return self._images.get("tvbanner", default) + + def get_disc(self, media_type, queryid, default=None): + """ + 获取光盘封面 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("moviedisc", default) + else: + return None + + def get_logo(self, media_type, queryid, default=None): + """ + 获取海报 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("hdmovielogo", default) + else: + return self._images.get("hdtvlogo", default) + + def get_thumb(self, media_type, queryid, default=None): + """ + 获取缩略图 + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.MOVIE: + return self._images.get("moviethumb", default) + else: + return self._images.get("tvthumb", default) + + def get_clearart(self, media_type, queryid, default=None): + """ + 获取clearart + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type == MediaType.TV: + return self._images.get("hdclearart", default) + else: + return None + + def get_seasonposter(self, media_type, queryid, season, default=None): + """ + 获取seasonposter + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type != MediaType.TV: + return None + return self._images.get("seasonposter", {}).get(season, "") or default + + def get_seasonthumb(self, media_type, queryid, season, default=None): + """ + 获取seasonposter + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type != MediaType.TV: + return None + return self._images.get("seasonthumb", {}).get(season, "") or default + + def get_seasonbanner(self, media_type, queryid, season, default=None): + """ + 获取seasonbanner + """ + if not media_type or not queryid: + return None + if not self._images: + self.__get_fanart_images(media_type=media_type, queryid=queryid) + if media_type != MediaType.TV: + return None + return self._images.get("seasonbanner", {}).get(season, "") or default diff --git a/app/media/media.py b/app/media/media.py new file mode 100644 index 0000000..3384f2b --- /dev/null +++ b/app/media/media.py @@ -0,0 +1,2131 @@ +import difflib +import os +import random +import re +import traceback +from functools import lru_cache + +import zhconv +from lxml import etree + +import log +from app.helper import MetaHelper +from app.media.meta.metainfo import MetaInfo +from app.media.tmdbv3api import TMDb, Search, Movie, TV, Person, Find, TMDbException, Discover, Trending, Episode, Genre +from app.utils import PathUtils, EpisodeFormat, RequestUtils, NumberUtils, StringUtils, cacheman +from app.utils.types import MediaType, MatchMode +from config import Config, KEYWORD_BLACKLIST, KEYWORD_SEARCH_WEIGHT_3, KEYWORD_SEARCH_WEIGHT_2, KEYWORD_SEARCH_WEIGHT_1, \ + KEYWORD_STR_SIMILARITY_THRESHOLD, KEYWORD_DIFF_SCORE_THRESHOLD, TMDB_IMAGE_ORIGINAL_URL, DEFAULT_TMDB_PROXY, \ + TMDB_IMAGE_FACE_URL, TMDB_PEOPLE_PROFILE_URL, TMDB_IMAGE_W500_URL + + +class Media: + # TheMovieDB + tmdb = None + search = None + movie = None + tv = None + episode = None + person = None + find = None + trending = None + discover = None + genre = None + meta = None + _rmt_match_mode = None + _search_keyword = None + _search_tmdbweb = None + + def __init__(self): + self.init_config() + + def init_config(self): + app = Config().get_config('app') + laboratory = Config().get_config('laboratory') + if app: + if app.get('rmt_tmdbkey'): + self.tmdb = TMDb() + if laboratory.get('tmdb_proxy'): + self.tmdb.domain = DEFAULT_TMDB_PROXY + else: + self.tmdb.domain = app.get("tmdb_domain") + self.tmdb.cache = True + self.tmdb.api_key = app.get('rmt_tmdbkey') + self.tmdb.language = 'zh' + self.tmdb.proxies = Config().get_proxies() + self.tmdb.debug = True + self.search = Search() + self.movie = Movie() + self.tv = TV() + self.episode = Episode() + self.find = Find() + self.person = Person() + self.trending = Trending() + self.discover = Discover() + self.genre = Genre() + self.meta = MetaHelper() + rmt_match_mode = app.get('rmt_match_mode', 'normal') + if rmt_match_mode: + rmt_match_mode = rmt_match_mode.upper() + else: + rmt_match_mode = "NORMAL" + if rmt_match_mode == "STRICT": + self._rmt_match_mode = MatchMode.STRICT + else: + self._rmt_match_mode = MatchMode.NORMAL + laboratory = Config().get_config('laboratory') + if laboratory: + self._search_keyword = laboratory.get("search_keyword") + self._search_tmdbweb = laboratory.get("search_tmdbweb") + + @staticmethod + def __compare_tmdb_names(file_name, tmdb_names): + """ + 比较文件名是否匹配,忽略大小写和特殊字符 + :param file_name: 识别的文件名或者种子名 + :param tmdb_names: TMDB返回的译名 + :return: True or False + """ + if not file_name or not tmdb_names: + return False + if not isinstance(tmdb_names, list): + tmdb_names = [tmdb_names] + file_name = StringUtils.handler_special_chars(file_name).upper() + for tmdb_name in tmdb_names: + tmdb_name = StringUtils.handler_special_chars(tmdb_name).strip().upper() + if file_name == tmdb_name: + return True + return False + + def __search_tmdb_allnames(self, mtype: MediaType, tmdb_id): + """ + 检索tmdb中所有的标题和译名,用于名称匹配 + :param mtype: 类型:电影、电视剧、动漫 + :param tmdb_id: TMDB的ID + :return: 所有译名的清单 + """ + if not mtype or not tmdb_id: + return {}, [] + ret_names = [] + tmdb_info = self.get_tmdb_info(mtype=mtype, tmdbid=tmdb_id) + if not tmdb_info: + return tmdb_info, [] + if mtype == MediaType.MOVIE: + alternative_titles = tmdb_info.get("alternative_titles", {}).get("titles", []) + for alternative_title in alternative_titles: + title = alternative_title.get("title") + if title and title not in ret_names: + ret_names.append(title) + translations = tmdb_info.get("translations", {}).get("translations", []) + for translation in translations: + title = translation.get("data", {}).get("title") + if title and title not in ret_names: + ret_names.append(title) + else: + alternative_titles = tmdb_info.get("alternative_titles", {}).get("results", []) + for alternative_title in alternative_titles: + name = alternative_title.get("title") + if name and name not in ret_names: + ret_names.append(name) + translations = tmdb_info.get("translations", {}).get("translations", []) + for translation in translations: + name = translation.get("data", {}).get("name") + if name and name not in ret_names: + ret_names.append(name) + return tmdb_info, ret_names + + def __search_tmdb(self, file_media_name, + search_type, + first_media_year=None, + media_year=None, + season_number=None, + language=None): + """ + 检索tmdb中的媒体信息,匹配返回一条尽可能正确的信息 + :param file_media_name: 剑索的名称 + :param search_type: 类型:电影、电视剧、动漫 + :param first_media_year: 年份,如要是季集需要是首播年份(first_air_date) + :param media_year: 当前季集年份 + :param season_number: 季集,整数 + :param language: 语言,默认是zh-CN + :return: TMDB的INFO,同时会将search_type赋值到media_type中 + """ + if not self.search: + return None + if not file_media_name: + return None + if language: + self.tmdb.language = language + else: + self.tmdb.language = 'zh-CN' + # TMDB检索 + info = {} + if search_type == MediaType.MOVIE: + year_range = [first_media_year] + if first_media_year: + year_range.append(str(int(first_media_year) + 1)) + year_range.append(str(int(first_media_year) - 1)) + for year in year_range: + log.debug( + f"【Meta】正在识别{search_type.value}:{file_media_name}, 年份={year} ...") + info = self.__search_movie_by_name(file_media_name, year) + if info: + info['media_type'] = MediaType.MOVIE + log.info("【Meta】%s 识别到 电影:TMDBID=%s, 名称=%s, 上映日期=%s" % ( + file_media_name, + info.get('id'), + info.get('title'), + info.get('release_date'))) + break + else: + # 有当前季和当前季集年份,使用精确匹配 + if media_year and season_number: + log.debug( + f"【Meta】正在识别{search_type.value}:{file_media_name}, 季集={season_number}, 季集年份={media_year} ...") + info = self.__search_tv_by_season(file_media_name, + media_year, + season_number) + if not info: + log.debug( + f"【Meta】正在识别{search_type.value}:{file_media_name}, 年份={StringUtils.xstr(first_media_year)} ...") + info = self.__search_tv_by_name(file_media_name, + first_media_year) + if info: + info['media_type'] = MediaType.TV + log.info("【Meta】%s 识别到 电视剧:TMDBID=%s, 名称=%s, 首播日期=%s" % ( + file_media_name, + info.get('id'), + info.get('name'), + info.get('first_air_date'))) + # 返回 + if info: + return info + else: + log.info("【Meta】%s 以年份 %s 在TMDB中未找到%s信息!" % ( + file_media_name, StringUtils.xstr(first_media_year), search_type.value if search_type else "")) + return info + + def __search_movie_by_name(self, file_media_name, first_media_year): + """ + 根据名称查询电影TMDB匹配 + :param file_media_name: 识别的文件名或种子名 + :param first_media_year: 电影上映日期 + :return: 匹配的媒体信息 + """ + try: + if first_media_year: + movies = self.search.movies({"query": file_media_name, "year": first_media_year}) + else: + movies = self.search.movies({"query": file_media_name}) + except TMDbException as err: + log.error(f"【Meta】连接TMDB出错:{str(err)}") + return None + except Exception as e: + log.error(f"【Meta】连接TMDB出错:{str(e)}") + return None + log.debug(f"【Meta】API返回:{str(self.search.total_results)}") + if len(movies) == 0: + log.debug(f"【Meta】{file_media_name} 未找到相关电影信息!") + return {} + else: + info = {} + if first_media_year: + for movie in movies: + if movie.get('release_date'): + if self.__compare_tmdb_names(file_media_name, movie.get('title')) \ + and movie.get('release_date')[0:4] == str(first_media_year): + return movie + if self.__compare_tmdb_names(file_media_name, movie.get('original_title')) \ + and movie.get('release_date')[0:4] == str(first_media_year): + return movie + else: + for movie in movies: + if self.__compare_tmdb_names(file_media_name, movie.get('title')) \ + or self.__compare_tmdb_names(file_media_name, movie.get('original_title')): + return movie + if not info: + index = 0 + for movie in movies: + if first_media_year: + if not movie.get('release_date'): + continue + if movie.get('release_date')[0:4] != str(first_media_year): + continue + index += 1 + info, names = self.__search_tmdb_allnames(MediaType.MOVIE, movie.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + return info + else: + index += 1 + info, names = self.__search_tmdb_allnames(MediaType.MOVIE, movie.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + return info + if index > 5: + break + return {} + + def __search_tv_by_name(self, file_media_name, first_media_year): + """ + 根据名称查询电视剧TMDB匹配 + :param file_media_name: 识别的文件名或者种子名 + :param first_media_year: 电视剧的首播年份 + :return: 匹配的媒体信息 + """ + try: + if first_media_year: + tvs = self.search.tv_shows({"query": file_media_name, "first_air_date_year": first_media_year}) + else: + tvs = self.search.tv_shows({"query": file_media_name}) + except TMDbException as err: + log.error(f"【Meta】连接TMDB出错:{str(err)}") + return None + except Exception as e: + log.error(f"【Meta】连接TMDB出错:{str(e)}") + return None + log.debug(f"【Meta】API返回:{str(self.search.total_results)}") + if len(tvs) == 0: + log.debug(f"【Meta】{file_media_name} 未找到相关剧集信息!") + return {} + else: + info = {} + if first_media_year: + for tv in tvs: + if tv.get('first_air_date'): + if self.__compare_tmdb_names(file_media_name, tv.get('name')) \ + and tv.get('first_air_date')[0:4] == str(first_media_year): + return tv + if self.__compare_tmdb_names(file_media_name, tv.get('original_name')) \ + and tv.get('first_air_date')[0:4] == str(first_media_year): + return tv + else: + for tv in tvs: + if self.__compare_tmdb_names(file_media_name, tv.get('name')) \ + or self.__compare_tmdb_names(file_media_name, tv.get('original_name')): + return tv + if not info: + index = 0 + for tv in tvs: + if first_media_year: + if not tv.get('first_air_date'): + continue + if tv.get('first_air_date')[0:4] != str(first_media_year): + continue + index += 1 + info, names = self.__search_tmdb_allnames(MediaType.TV, tv.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + return info + else: + index += 1 + info, names = self.__search_tmdb_allnames(MediaType.TV, tv.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + return info + if index > 5: + break + return {} + + def __search_tv_by_season(self, file_media_name, media_year, season_number): + """ + 根据电视剧的名称和季的年份及序号匹配TMDB + :param file_media_name: 识别的文件名或者种子名 + :param media_year: 季的年份 + :param season_number: 季序号 + :return: 匹配的媒体信息 + """ + + def __season_match(tv_info, season_year): + if not tv_info: + return False + try: + seasons = self.get_tmdb_tv_seasons(tv_info=tv_info) + for season in seasons: + if season.get("air_date") and season.get("season_number"): + if season.get("air_date")[0:4] == str(season_year) \ + and season.get("season_number") == int(season_number): + return True + except Exception as e1: + log.error(f"【Meta】连接TMDB出错:{e1}") + return False + return False + + try: + tvs = self.search.tv_shows({"query": file_media_name}) + except TMDbException as err: + log.error(f"【Meta】连接TMDB出错:{str(err)}") + return None + except Exception as e: + log.error(f"【Meta】连接TMDB出错:{e}") + return None + + if len(tvs) == 0: + log.debug("【Meta】%s 未找到季%s相关信息!" % (file_media_name, season_number)) + return {} + else: + for tv in tvs: + if (self.__compare_tmdb_names(file_media_name, tv.get('name')) + or self.__compare_tmdb_names(file_media_name, tv.get('original_name'))) \ + and (tv.get('first_air_date') and tv.get('first_air_date')[0:4] == str(media_year)): + return tv + + for tv in tvs[:5]: + info, names = self.__search_tmdb_allnames(MediaType.TV, tv.get("id")) + if not self.__compare_tmdb_names(file_media_name, names): + continue + if __season_match(tv_info=info, season_year=media_year): + return info + return {} + + def __search_multi_tmdb(self, file_media_name): + """ + 根据名称同时查询电影和电视剧,不带年份 + :param file_media_name: 识别的文件名或种子名 + :return: 匹配的媒体信息 + """ + try: + multis = self.search.multi({"query": file_media_name}) or [] + except TMDbException as err: + log.error(f"【Meta】连接TMDB出错:{str(err)}") + return None + except Exception as e: + log.error(f"【Meta】连接TMDB出错:{str(e)}") + return None + log.debug(f"【Meta】API返回:{str(self.search.total_results)}") + if len(multis) == 0: + log.debug(f"【Meta】{file_media_name} 未找到相关媒体息!") + return {} + else: + info = {} + for multi in multis: + if multi.get("media_type") == "movie": + if self.__compare_tmdb_names(file_media_name, multi.get('title')) \ + or self.__compare_tmdb_names(file_media_name, multi.get('original_title')): + info = multi + elif multi.get("media_type") == "tv": + if self.__compare_tmdb_names(file_media_name, multi.get('name')) \ + or self.__compare_tmdb_names(file_media_name, multi.get('original_name')): + info = multi + if not info: + for multi in multis[:5]: + if multi.get("media_type") == "movie": + movie_info, names = self.__search_tmdb_allnames(MediaType.MOVIE, multi.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + info = movie_info + elif multi.get("media_type") == "tv": + tv_info, names = self.__search_tmdb_allnames(MediaType.TV, multi.get("id")) + if self.__compare_tmdb_names(file_media_name, names): + info = tv_info + # 返回 + if info: + info['media_type'] = MediaType.MOVIE if info.get('media_type') == 'movie' else MediaType.TV + return info + else: + log.info("【Meta】%s 在TMDB中未找到媒体信息!" % file_media_name) + return info + + @lru_cache(maxsize=128) + def __search_tmdb_web(self, file_media_name, mtype: MediaType): + """ + 检索TMDB网站,直接抓取结果,结果只有一条时才返回 + :param file_media_name: 名称 + """ + if not file_media_name: + return None + if StringUtils.is_chinese(file_media_name): + return {} + log.info("【Meta】正在从TheDbMovie网站查询:%s ..." % file_media_name) + tmdb_url = "https://www.themoviedb.org/search?query=%s" % file_media_name + res = RequestUtils(timeout=5).get_res(url=tmdb_url) + if res and res.status_code == 200: + html_text = res.text + if not html_text: + return None + try: + tmdb_links = [] + html = etree.HTML(html_text) + links = html.xpath("//a[@data-id]/@href") + for link in links: + if not link or (not link.startswith("/tv") and not link.startswith("/movie")): + continue + if link not in tmdb_links: + tmdb_links.append(link) + if len(tmdb_links) == 1: + tmdbinfo = self.get_tmdb_info( + mtype=MediaType.TV if tmdb_links[0].startswith("/tv") else MediaType.MOVIE, + tmdbid=tmdb_links[0].split("/")[-1]) + if tmdbinfo: + if mtype == MediaType.TV and tmdbinfo.get('media_type') != MediaType.TV: + return {} + if tmdbinfo.get('media_type') == MediaType.MOVIE: + log.info("【Meta】%s 从WEB识别到 电影:TMDBID=%s, 名称=%s, 上映日期=%s" % ( + file_media_name, + tmdbinfo.get('id'), + tmdbinfo.get('title'), + tmdbinfo.get('release_date'))) + else: + log.info("【Meta】%s 从WEB识别到 电视剧:TMDBID=%s, 名称=%s, 首播日期=%s" % ( + file_media_name, + tmdbinfo.get('id'), + tmdbinfo.get('name'), + tmdbinfo.get('first_air_date'))) + return tmdbinfo + elif len(tmdb_links) > 1: + log.info("【Meta】%s TMDB网站返回数据过多:%s" % (file_media_name, len(tmdb_links))) + else: + log.info("【Meta】%s TMDB网站未查询到媒体信息!" % file_media_name) + except Exception as err: + print(str(err)) + return None + return None + + def get_tmdb_info(self, mtype: MediaType, + tmdbid, + language=None, + append_to_response=None, + chinese=True): + """ + 给定TMDB号,查询一条媒体信息 + :param mtype: 类型:电影、电视剧、动漫,为空时都查(此时用不上年份) + :param tmdbid: TMDB的ID,有tmdbid时优先使用tmdbid,否则使用年份和标题 + :param language: 语种 + :param append_to_response: 附加信息 + :param chinese: 是否转换中文标题 + """ + if not self.tmdb: + log.error("【Meta】TMDB API Key 未设置!") + return None + if language: + self.tmdb.language = language + else: + self.tmdb.language = 'zh-CN' + if mtype == MediaType.MOVIE: + tmdb_info = self.__get_tmdb_movie_detail(tmdbid, append_to_response) + if tmdb_info: + tmdb_info['media_type'] = MediaType.MOVIE + else: + tmdb_info = self.__get_tmdb_tv_detail(tmdbid, append_to_response) + if tmdb_info: + tmdb_info['media_type'] = MediaType.TV + if tmdb_info: + # 转换genreid + tmdb_info['genre_ids'] = self.__get_genre_ids_from_detail(tmdb_info.get('genres')) + # 转换中文标题 + if chinese: + tmdb_info = self.__update_tmdbinfo_cn_title(tmdb_info) + + return tmdb_info + + def __update_tmdbinfo_cn_title(self, tmdb_info): + """ + 更新TMDB信息中的中文名称 + """ + # 查找中文名 + org_title = tmdb_info.get("title") if tmdb_info.get("media_type") == MediaType.MOVIE else tmdb_info.get( + "name") + if not StringUtils.is_chinese(org_title) and self.tmdb.language == 'zh-CN': + cn_title = self.__get_tmdb_chinese_title(tmdbinfo=tmdb_info) + if cn_title and cn_title != org_title: + if tmdb_info.get("media_type") == MediaType.MOVIE: + tmdb_info['title'] = cn_title + else: + tmdb_info['name'] = cn_title + return tmdb_info + + def get_tmdb_infos(self, title, year=None, mtype: MediaType = None, page=1): + """ + 查询名称中有关键字的所有的TMDB信息并返回 + """ + if not self.tmdb: + log.error("【Meta】TMDB API Key 未设置!") + return [] + if not title: + return [] + if not mtype and not year: + results = self.__search_multi_tmdbinfos(title) + else: + if not mtype: + results = list( + set(self.__search_movie_tmdbinfos(title, year)).union(set(self.__search_tv_tmdbinfos(title, year)))) + # 组合结果的情况下要排序 + results = sorted(results, + key=lambda x: x.get("release_date") or x.get("first_air_date") or "0000-00-00", + reverse=True) + elif mtype == MediaType.MOVIE: + results = self.__search_movie_tmdbinfos(title, year) + else: + results = self.__search_tv_tmdbinfos(title, year) + return results[(page - 1) * 20:page * 20] + + def __search_multi_tmdbinfos(self, title): + """ + 同时查询模糊匹配的电影、电视剧TMDB信息 + """ + if not title: + return [] + ret_infos = [] + multis = self.search.multi({"query": title}) or [] + for multi in multis: + if multi.get("media_type") in ["movie", "tv"]: + multi['media_type'] = MediaType.MOVIE if multi.get("media_type") == "movie" else MediaType.TV + ret_infos.append(multi) + return ret_infos + + def __search_movie_tmdbinfos(self, title, year): + """ + 查询模糊匹配的所有电影TMDB信息 + """ + if not title: + return [] + ret_infos = [] + if year: + movies = self.search.movies({"query": title, "year": year}) or [] + else: + movies = self.search.movies({"query": title}) or [] + for movie in movies: + if title in movie.get("title"): + movie['media_type'] = MediaType.MOVIE + ret_infos.append(movie) + return ret_infos + + def __search_tv_tmdbinfos(self, title, year): + """ + 查询模糊匹配的所有电视剧TMDB信息 + """ + if not title: + return [] + ret_infos = [] + if year: + tvs = self.search.tv_shows({"query": title, "first_air_date_year": year}) or [] + else: + tvs = self.search.tv_shows({"query": title}) or [] + for tv in tvs: + if title in tv.get("name"): + tv['media_type'] = MediaType.TV + ret_infos.append(tv) + return ret_infos + + @staticmethod + def __make_cache_key(meta_info): + """ + 生成缓存的key + """ + if not meta_info: + return None + return f"[{meta_info.type.value}]{meta_info.get_name()}-{meta_info.year}-{meta_info.begin_season}" + + def get_cache_info(self, meta_info): + """ + 根据名称查询是否已经有缓存 + """ + if not meta_info: + return {} + return self.meta.get_meta_data_by_key(self.__make_cache_key(meta_info)) + + def get_media_info(self, title, + subtitle=None, + mtype=None, + strict=None, + cache=True, + chinese=True, + append_to_response=None): + """ + 只有名称信息,判别是电影还是电视剧并搜刮TMDB信息,用于种子名称识别 + :param title: 种子名称 + :param subtitle: 种子副标题 + :param mtype: 类型:电影、电视剧、动漫 + :param strict: 是否严格模式,为true时,不会再去掉年份再查一次 + :param cache: 是否使用缓存,默认TRUE + :param chinese: 原标题为英文时是否从别名中检索中文名称 + :param append_to_response: 额外查询的信息 + :return: 带有TMDB信息的MetaInfo对象 + """ + if not self.tmdb: + log.error("【Meta】TMDB API Key 未设置!") + return None + if not title: + return None + # 识别 + meta_info = MetaInfo(title, subtitle=subtitle) + if not meta_info.get_name() or not meta_info.type: + log.warn("【Rmt】%s 未识别出有效信息!" % meta_info.org_string) + return None + if mtype: + meta_info.type = mtype + media_key = self.__make_cache_key(meta_info) + if not cache or not self.meta.get_meta_data_by_key(media_key): + # 缓存没有或者强制不使用缓存 + if meta_info.type != MediaType.TV and not meta_info.year: + file_media_info = self.__search_multi_tmdb(file_media_name=meta_info.get_name()) + else: + if meta_info.type == MediaType.TV: + # 确定是电视 + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + first_media_year=meta_info.year, + search_type=meta_info.type, + media_year=meta_info.year, + season_number=meta_info.begin_season + ) + if not file_media_info and meta_info.year and self._rmt_match_mode == MatchMode.NORMAL and not strict: + # 非严格模式下去掉年份再查一次 + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + search_type=meta_info.type + ) + else: + # 有年份先按电影查 + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + first_media_year=meta_info.year, + search_type=MediaType.MOVIE + ) + # 没有再按电视剧查 + if not file_media_info: + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + first_media_year=meta_info.year, + search_type=MediaType.TV + ) + if not file_media_info and self._rmt_match_mode == MatchMode.NORMAL and not strict: + # 非严格模式下去掉年份和类型再查一次 + file_media_info = self.__search_multi_tmdb(file_media_name=meta_info.get_name()) + if not file_media_info and self._search_tmdbweb: + file_media_info = self.__search_tmdb_web(file_media_name=meta_info.get_name(), + mtype=meta_info.type) + if not file_media_info and self._search_keyword: + cache_name = cacheman["tmdb_supply"].get(meta_info.get_name()) + is_movie = False + if not cache_name: + cache_name, is_movie = self.__search_engine(meta_info.get_name()) + cacheman["tmdb_supply"].set(meta_info.get_name(), cache_name) + if cache_name: + log.info("【Meta】开始辅助查询:%s ..." % cache_name) + if is_movie: + file_media_info = self.__search_tmdb(file_media_name=cache_name, search_type=MediaType.MOVIE) + else: + file_media_info = self.__search_multi_tmdb(file_media_name=cache_name) + # 补充全量信息 + if file_media_info and not file_media_info.get("genres"): + file_media_info = self.get_tmdb_info(mtype=file_media_info.get("media_type"), + tmdbid=file_media_info.get("id"), + chinese=chinese, + append_to_response=append_to_response) + # 保存到缓存 + if file_media_info is not None: + self.__insert_media_cache(media_key=media_key, + file_media_info=file_media_info) + else: + # 使用缓存信息 + cache_info = self.meta.get_meta_data_by_key(media_key) + if cache_info.get("id"): + file_media_info = self.get_tmdb_info(mtype=cache_info.get("type"), + tmdbid=cache_info.get("id"), + chinese=chinese, + append_to_response=append_to_response) + else: + file_media_info = None + # 赋值TMDB信息并返回 + meta_info.set_tmdb_info(file_media_info) + return meta_info + + def __insert_media_cache(self, media_key, file_media_info): + """ + 将TMDB信息插入缓存 + """ + if file_media_info: + # 缓存标题 + cache_title = file_media_info.get( + "title") if file_media_info.get( + "media_type") == MediaType.MOVIE else file_media_info.get("name") + # 缓存年份 + cache_year = file_media_info.get('release_date') if file_media_info.get( + "media_type") == MediaType.MOVIE else file_media_info.get('first_air_date') + if cache_year: + cache_year = cache_year[:4] + self.meta.update_meta_data({ + media_key: { + "id": file_media_info.get("id"), + "type": file_media_info.get("media_type"), + "year": cache_year, + "title": cache_title, + "poster_path": file_media_info.get("poster_path"), + "backdrop_path": file_media_info.get("backdrop_path") + } + }) + else: + self.meta.update_meta_data({media_key: {'id': 0}}) + + def get_media_info_on_files(self, + file_list, + tmdb_info=None, + media_type=None, + season=None, + episode_format: EpisodeFormat = None, + chinese=True): + """ + 根据文件清单,搜刮TMDB信息,用于文件名称的识别 + :param file_list: 文件清单,如果是列表也可以是单个文件,也可以是一个目录 + :param tmdb_info: 如有传入TMDB信息则以该TMDB信息赋于所有文件,否则按名称从TMDB检索,用于手工识别时传入 + :param media_type: 媒体类型:电影、电视剧、动漫,如有传入以该类型赋于所有文件,否则按名称从TMDB检索并识别 + :param season: 季号,如有传入以该季号赋于所有文件,否则从名称中识别 + :param episode_format: EpisodeFormat + :param chinese: 原标题为英文时是否从别名中检索中文名称 + :return: 带有TMDB信息的每个文件对应的MetaInfo对象字典 + """ + # 存储文件路径与媒体的对应关系 + if not self.tmdb: + log.error("【Meta】TMDB API Key 未设置!") + return {} + return_media_infos = {} + # 不是list的转为list + if not isinstance(file_list, list): + file_list = [file_list] + # 遍历每个文件,看得出来的名称是不是不一样,不一样的先搜索媒体信息 + for file_path in file_list: + try: + if not os.path.exists(file_path): + log.warn("【Meta】%s 不存在" % file_path) + continue + # 解析媒体名称 + # 先用自己的名称 + file_name = os.path.basename(file_path) + parent_name = os.path.basename(os.path.dirname(file_path)) + parent_parent_name = os.path.basename(PathUtils.get_parent_paths(file_path, 2)) + # 过滤掉蓝光原盘目录下的子文件 + if not os.path.isdir(file_path) \ + and PathUtils.get_bluray_dir(file_path): + log.info("【Meta】%s 跳过蓝光原盘文件:" % file_path) + continue + # 没有自带TMDB信息 + if not tmdb_info: + # 识别名称 + meta_info = MetaInfo(title=file_name) + # 识别不到则使用上级的名称 + if not meta_info.get_name() or not meta_info.year: + parent_info = MetaInfo(parent_name) + if not parent_info.get_name() or not parent_info.year: + parent_parent_info = MetaInfo(parent_parent_name) + parent_info.type = parent_parent_info.type if parent_parent_info.type and parent_info.type != MediaType.TV else parent_info.type + parent_info.cn_name = parent_parent_info.cn_name if parent_parent_info.cn_name else parent_info.cn_name + parent_info.en_name = parent_parent_info.en_name if parent_parent_info.en_name else parent_info.en_name + parent_info.year = parent_parent_info.year if parent_parent_info.year else parent_info.year + parent_info.begin_season = NumberUtils.max_ele(parent_info.begin_season, + parent_parent_info.begin_season) + if not meta_info.get_name(): + meta_info.cn_name = parent_info.cn_name + meta_info.en_name = parent_info.en_name + if not meta_info.year: + meta_info.year = parent_info.year + if parent_info.type and parent_info.type == MediaType.TV \ + and meta_info.type != MediaType.TV: + meta_info.type = parent_info.type + if meta_info.type == MediaType.TV: + meta_info.begin_season = NumberUtils.max_ele(parent_info.begin_season, + meta_info.begin_season) + if not meta_info.get_name() or not meta_info.type: + log.warn("【Rmt】%s 未识别出有效信息!" % meta_info.org_string) + continue + # 区配缓存及TMDB + media_key = self.__make_cache_key(meta_info) + if not self.meta.get_meta_data_by_key(media_key): + # 没有缓存数据 + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + first_media_year=meta_info.year, + search_type=meta_info.type, + media_year=meta_info.year, + season_number=meta_info.begin_season) + if not file_media_info: + if self._rmt_match_mode == MatchMode.NORMAL: + # 去掉年份再查一次,有可能是年份错误 + file_media_info = self.__search_tmdb(file_media_name=meta_info.get_name(), + search_type=meta_info.type) + if not file_media_info and self._search_tmdbweb: + # 从网站查询 + file_media_info = self.__search_tmdb_web(file_media_name=meta_info.get_name(), + mtype=meta_info.type) + if not file_media_info and self._search_keyword: + cache_name = cacheman["tmdb_supply"].get(meta_info.get_name()) + is_movie = False + if not cache_name: + cache_name, is_movie = self.__search_engine(meta_info.get_name()) + cacheman["tmdb_supply"].set(meta_info.get_name(), cache_name) + if cache_name: + log.info("【Meta】开始辅助查询:%s ..." % cache_name) + if is_movie: + file_media_info = self.__search_tmdb(file_media_name=cache_name, + search_type=MediaType.MOVIE) + else: + file_media_info = self.__search_multi_tmdb(file_media_name=cache_name) + # 补全TMDB信息 + if file_media_info and not file_media_info.get("genres"): + file_media_info = self.get_tmdb_info(mtype=file_media_info.get("media_type"), + tmdbid=file_media_info.get("id"), + chinese=chinese) + # 保存到缓存 + if file_media_info is not None: + self.__insert_media_cache(media_key=media_key, + file_media_info=file_media_info) + else: + # 使用缓存信息 + cache_info = self.meta.get_meta_data_by_key(media_key) + if cache_info.get("id"): + file_media_info = self.get_tmdb_info(mtype=cache_info.get("type"), + tmdbid=cache_info.get("id"), + chinese=chinese) + else: + # 缓存为未识别 + file_media_info = None + # 赋值TMDB信息 + meta_info.set_tmdb_info(file_media_info) + # 自带TMDB信息 + else: + meta_info = MetaInfo(title=file_name, mtype=media_type) + meta_info.set_tmdb_info(tmdb_info) + if season and meta_info.type != MediaType.MOVIE: + meta_info.begin_season = int(season) + if episode_format: + begin_ep, end_ep = episode_format.split_episode(file_name) + if begin_ep is not None: + meta_info.begin_episode = begin_ep + if end_ep is not None: + meta_info.end_episode = end_ep + # 加入缓存 + self.save_rename_cache(file_name, tmdb_info) + # 按文件路程存储 + return_media_infos[file_path] = meta_info + except Exception as err: + print(str(err)) + log.error("【Rmt】发生错误:%s - %s" % (str(err), traceback.format_exc())) + # 循环结束 + return return_media_infos + + @staticmethod + def __dict_tmdbinfos(infos, mtype=None): + """ + TMDB电影信息转为字典 + """ + if not infos: + return [] + ret_infos = [] + for info in infos: + tmdbid = info.get("id") + vote = round(float(info.get("vote_average")), 1) if info.get("vote_average") else 0, + image = TMDB_IMAGE_W500_URL % info.get("poster_path") + overview = info.get("overview") + if mtype: + media_type = mtype.value + year = info.get("release_date")[0:4] if info.get( + "release_date") and mtype == MediaType.MOVIE else info.get( + "first_air_date")[0:4] if info.get( + "first_air_date") else "" + typestr = 'MOV' if mtype == MediaType.MOVIE else 'TV' + title = info.get("title") if mtype == MediaType.MOVIE else info.get("name") + else: + media_type = MediaType.MOVIE.value if info.get( + "media_type") == "movie" else MediaType.TV.value + year = info.get("release_date")[0:4] if info.get( + "release_date") and info.get( + "media_type") == "movie" else info.get( + "first_air_date")[0:4] if info.get( + "first_air_date") else "" + typestr = 'MOV' if info.get("media_type") == "movie" else 'TV' + title = info.get("title") if info.get("media_type") == "movie" else info.get("name") + + ret_infos.append({ + 'id': tmdbid, + 'orgid': tmdbid, + 'tmdbid': tmdbid, + 'title': title, + 'type': typestr, + 'media_type': media_type, + 'year': year, + 'vote': vote, + 'image': image, + 'overview': overview + }) + + return ret_infos + + def get_tmdb_hot_movies(self, page): + """ + 获取热门电影 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.movie: + return [] + return self.__dict_tmdbinfos(self.movie.popular(page), MediaType.MOVIE) + + def get_tmdb_hot_tvs(self, page): + """ + 获取热门电视剧 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.tv: + return [] + return self.__dict_tmdbinfos(self.tv.popular(page), MediaType.TV) + + def get_tmdb_new_movies(self, page): + """ + 获取最新电影 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.movie: + return [] + return self.__dict_tmdbinfos(self.movie.now_playing(page), MediaType.MOVIE) + + def get_tmdb_new_tvs(self, page): + """ + 获取最新电视剧 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.tv: + return [] + return self.__dict_tmdbinfos(self.tv.on_the_air(page), MediaType.TV) + + def get_tmdb_upcoming_movies(self, page): + """ + 获取即将上映电影 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.movie: + return [] + return self.__dict_tmdbinfos(self.movie.upcoming(page), MediaType.MOVIE) + + def get_tmdb_trending_all_week(self, page=1): + """ + 获取即将上映电影 + :param page: 第几页 + :return: TMDB信息列表 + """ + if not self.movie: + return [] + return self.__dict_tmdbinfos(self.trending.all_week(page=page)) + + def __get_tmdb_movie_detail(self, tmdbid, append_to_response=None): + """ + 获取电影的详情 + :param tmdbid: TMDB ID + :return: TMDB信息 + """ + """ + { + "adult": false, + "backdrop_path": "/r9PkFnRUIthgBp2JZZzD380MWZy.jpg", + "belongs_to_collection": { + "id": 94602, + "name": "穿靴子的猫(系列)", + "poster_path": "/anHwj9IupRoRZZ98WTBvHpTiE6A.jpg", + "backdrop_path": "/feU1DWV5zMWxXUHJyAIk3dHRQ9c.jpg" + }, + "budget": 90000000, + "genres": [ + { + "id": 16, + "name": "动画" + }, + { + "id": 28, + "name": "动作" + }, + { + "id": 12, + "name": "冒险" + }, + { + "id": 35, + "name": "喜剧" + }, + { + "id": 10751, + "name": "家庭" + }, + { + "id": 14, + "name": "奇幻" + } + ], + "homepage": "", + "id": 315162, + "imdb_id": "tt3915174", + "original_language": "en", + "original_title": "Puss in Boots: The Last Wish", + "overview": "时隔11年,臭屁自大又爱卖萌的猫大侠回来了!如今的猫大侠(安东尼奥·班德拉斯 配音),依旧幽默潇洒又不拘小节、数次“花式送命”后,九条命如今只剩一条,于是不得不请求自己的老搭档兼“宿敌”——迷人的软爪妞(萨尔玛·海耶克 配音)来施以援手来恢复自己的九条生命。", + "popularity": 8842.129, + "poster_path": "/rnn30OlNPiC3IOoWHKoKARGsBRK.jpg", + "production_companies": [ + { + "id": 33, + "logo_path": "/8lvHyhjr8oUKOOy2dKXoALWKdp0.png", + "name": "Universal Pictures", + "origin_country": "US" + }, + { + "id": 521, + "logo_path": "/kP7t6RwGz2AvvTkvnI1uteEwHet.png", + "name": "DreamWorks Animation", + "origin_country": "US" + } + ], + "production_countries": [ + { + "iso_3166_1": "US", + "name": "United States of America" + } + ], + "release_date": "2022-12-07", + "revenue": 260725470, + "runtime": 102, + "spoken_languages": [ + { + "english_name": "English", + "iso_639_1": "en", + "name": "English" + }, + { + "english_name": "Spanish", + "iso_639_1": "es", + "name": "Español" + } + ], + "status": "Released", + "tagline": "", + "title": "穿靴子的猫2", + "video": false, + "vote_average": 8.614, + "vote_count": 2291 + } + """ + if not self.movie: + return {} + try: + log.info("【Meta】正在查询TMDB电影:%s ..." % tmdbid) + tmdbinfo = self.movie.details(tmdbid, append_to_response) + return tmdbinfo or {} + except Exception as e: + print(str(e)) + return None + + def __get_tmdb_tv_detail(self, tmdbid, append_to_response=None): + """ + 获取电视剧的详情 + :param tmdbid: TMDB ID + :return: TMDB信息 + """ + """ + { + "adult": false, + "backdrop_path": "/uDgy6hyPd82kOHh6I95FLtLnj6p.jpg", + "created_by": [ + { + "id": 35796, + "credit_id": "5e84f06a3344c600153f6a57", + "name": "Craig Mazin", + "gender": 2, + "profile_path": "/uEhna6qcMuyU5TP7irpTUZ2ZsZc.jpg" + }, + { + "id": 1295692, + "credit_id": "5e84f03598f1f10016a985c0", + "name": "Neil Druckmann", + "gender": 2, + "profile_path": "/bVUsM4aYiHbeSYE1xAw2H5Z1ANU.jpg" + } + ], + "episode_run_time": [], + "first_air_date": "2023-01-15", + "genres": [ + { + "id": 18, + "name": "剧情" + }, + { + "id": 10765, + "name": "Sci-Fi & Fantasy" + }, + { + "id": 10759, + "name": "动作冒险" + } + ], + "homepage": "https://www.hbo.com/the-last-of-us", + "id": 100088, + "in_production": true, + "languages": [ + "en" + ], + "last_air_date": "2023-01-15", + "last_episode_to_air": { + "air_date": "2023-01-15", + "episode_number": 1, + "id": 2181581, + "name": "当你迷失在黑暗中", + "overview": "在一场全球性的流行病摧毁了文明之后,一个顽强的幸存者负责照顾一个 14 岁的小女孩,她可能是人类最后的希望。", + "production_code": "", + "runtime": 81, + "season_number": 1, + "show_id": 100088, + "still_path": "/aRquEWm8wWF1dfa9uZ1TXLvVrKD.jpg", + "vote_average": 8, + "vote_count": 33 + }, + "name": "最后生还者", + "next_episode_to_air": { + "air_date": "2023-01-22", + "episode_number": 2, + "id": 4071039, + "name": "虫草变异菌", + "overview": "", + "production_code": "", + "runtime": 55, + "season_number": 1, + "show_id": 100088, + "still_path": "/jkUtYTmeap6EvkHI4n0j5IRFrIr.jpg", + "vote_average": 10, + "vote_count": 1 + }, + "networks": [ + { + "id": 49, + "name": "HBO", + "logo_path": "/tuomPhY2UtuPTqqFnKMVHvSb724.png", + "origin_country": "US" + } + ], + "number_of_episodes": 9, + "number_of_seasons": 1, + "origin_country": [ + "US" + ], + "original_language": "en", + "original_name": "The Last of Us", + "overview": "不明真菌疫情肆虐之后的美国,被真菌感染的人都变成了可怕的怪物,乔尔(Joel)为了换回武器答应将小女孩儿艾莉(Ellie)送到指定地点,由此开始了两人穿越美国的漫漫旅程。", + "popularity": 5585.639, + "poster_path": "/nOY3VBFO0VnlN9nlRombnMTztyh.jpg", + "production_companies": [ + { + "id": 3268, + "logo_path": "/tuomPhY2UtuPTqqFnKMVHvSb724.png", + "name": "HBO", + "origin_country": "US" + }, + { + "id": 11073, + "logo_path": "/aCbASRcI1MI7DXjPbSW9Fcv9uGR.png", + "name": "Sony Pictures Television Studios", + "origin_country": "US" + }, + { + "id": 23217, + "logo_path": "/kXBZdQigEf6QiTLzo6TFLAa7jKD.png", + "name": "Naughty Dog", + "origin_country": "US" + }, + { + "id": 115241, + "logo_path": null, + "name": "The Mighty Mint", + "origin_country": "US" + }, + { + "id": 119645, + "logo_path": null, + "name": "Word Games", + "origin_country": "US" + }, + { + "id": 125281, + "logo_path": "/3hV8pyxzAJgEjiSYVv1WZ0ZYayp.png", + "name": "PlayStation Productions", + "origin_country": "US" + } + ], + "production_countries": [ + { + "iso_3166_1": "US", + "name": "United States of America" + } + ], + "seasons": [ + { + "air_date": "2023-01-15", + "episode_count": 9, + "id": 144593, + "name": "第 1 季", + "overview": "", + "poster_path": "/aUQKIpZZ31KWbpdHMCmaV76u78T.jpg", + "season_number": 1 + } + ], + "spoken_languages": [ + { + "english_name": "English", + "iso_639_1": "en", + "name": "English" + } + ], + "status": "Returning Series", + "tagline": "", + "type": "Scripted", + "vote_average": 8.924, + "vote_count": 601 + } + """ + if not self.tv: + return {} + try: + log.info("【Meta】正在查询TMDB电视剧:%s ..." % tmdbid) + tmdbinfo = self.tv.details(tmdbid, append_to_response) + return tmdbinfo or {} + except Exception as e: + print(str(e)) + return None + + def get_tmdb_tv_season_detail(self, tmdbid, season: int): + """ + 获取电视剧季的详情 + :param tmdbid: TMDB ID + :param season: 季,数字 + :return: TMDB信息 + """ + """ + { + "_id": "5e614cd3357c00001631a6ef", + "air_date": "2023-01-15", + "episodes": [ + { + "air_date": "2023-01-15", + "episode_number": 1, + "id": 2181581, + "name": "当你迷失在黑暗中", + "overview": "在一场全球性的流行病摧毁了文明之后,一个顽强的幸存者负责照顾一个 14 岁的小女孩,她可能是人类最后的希望。", + "production_code": "", + "runtime": 81, + "season_number": 1, + "show_id": 100088, + "still_path": "/aRquEWm8wWF1dfa9uZ1TXLvVrKD.jpg", + "vote_average": 8, + "vote_count": 33, + "crew": [ + { + "job": "Writer", + "department": "Writing", + "credit_id": "619c370063536a00619a08ee", + "adult": false, + "gender": 2, + "id": 35796, + "known_for_department": "Writing", + "name": "Craig Mazin", + "original_name": "Craig Mazin", + "popularity": 15.211, + "profile_path": "/uEhna6qcMuyU5TP7irpTUZ2ZsZc.jpg" + }, + ], + "guest_stars": [ + { + "character": "Marlene", + "credit_id": "63c4ca5e5f2b8d00aed539fc", + "order": 500, + "adult": false, + "gender": 1, + "id": 1253388, + "known_for_department": "Acting", + "name": "Merle Dandridge", + "original_name": "Merle Dandridge", + "popularity": 21.679, + "profile_path": "/lKwHdTtDf6NGw5dUrSXxbfkZLEk.jpg" + } + ] + }, + ], + "name": "第 1 季", + "overview": "", + "id": 144593, + "poster_path": "/aUQKIpZZ31KWbpdHMCmaV76u78T.jpg", + "season_number": 1 + } + """ + if not self.tv: + return {} + try: + log.info("【Meta】正在查询TMDB电视剧:%s,季:%s ..." % (tmdbid, season)) + tmdbinfo = self.tv.season_details(tmdbid, season) + return tmdbinfo or {} + except Exception as e: + print(str(e)) + return {} + + def get_tmdb_tv_seasons_byid(self, tmdbid): + """ + 根据TMDB查询TMDB电视剧的所有季 + """ + if not tmdbid: + return [] + return self.get_tmdb_tv_seasons( + tv_info=self.__get_tmdb_tv_detail( + tmdbid=tmdbid + ) + ) + + @staticmethod + def get_tmdb_tv_seasons(tv_info): + """ + 查询TMDB电视剧的所有季 + :param tv_info: TMDB 的季信息 + :return: 带有season_number、episode_count 的每季总集数的字典列表 + """ + """ + "seasons": [ + { + "air_date": "2006-01-08", + "episode_count": 11, + "id": 3722, + "name": "特别篇", + "overview": "", + "poster_path": "/snQYndfsEr3Sto2jOmkmsQuUXAQ.jpg", + "season_number": 0 + }, + { + "air_date": "2005-03-27", + "episode_count": 9, + "id": 3718, + "name": "第 1 季", + "overview": "", + "poster_path": "/foM4ImvUXPrD2NvtkHyixq5vhPx.jpg", + "season_number": 1 + } + ] + """ + if not tv_info: + return [] + return tv_info.get("seasons") or [] + + def get_tmdb_season_episodes(self, tmdbid, season: int): + """ + :param: tmdbid: TMDB ID + :param: season: 季号 + """ + """ + 从TMDB的季集信息中获得某季的集信息 + """ + """ + "episodes": [ + { + "air_date": "2023-01-15", + "episode_number": 1, + "id": 2181581, + "name": "当你迷失在黑暗中", + "overview": "在一场全球性的流行病摧毁了文明之后,一个顽强的幸存者负责照顾一个 14 岁的小女孩,她可能是人类最后的希望。", + "production_code": "", + "runtime": 81, + "season_number": 1, + "show_id": 100088, + "still_path": "/aRquEWm8wWF1dfa9uZ1TXLvVrKD.jpg", + "vote_average": 8, + "vote_count": 33 + }, + ] + """ + if not tmdbid: + return [] + season_info = self.get_tmdb_tv_season_detail(tmdbid=tmdbid, season=season) + if not season_info: + return [] + return season_info.get("episodes") or [] + + @staticmethod + def get_tmdb_backdrops(tmdbinfo): + """ + 获取TMDB的背景图 + """ + """ + { + "backdrops": [ + { + "aspect_ratio": 1.778, + "height": 2160, + "iso_639_1": "en", + "file_path": "/qUroDlCDUMwRWbkyjZGB9THkMgZ.jpg", + "vote_average": 5.312, + "vote_count": 1, + "width": 3840 + }, + { + "aspect_ratio": 1.778, + "height": 2160, + "iso_639_1": "en", + "file_path": "/iyxvxEQIfQjzJJTfszZxmH5UV35.jpg", + "vote_average": 0, + "vote_count": 0, + "width": 3840 + }, + { + "aspect_ratio": 1.778, + "height": 720, + "iso_639_1": "en", + "file_path": "/8SRY6IcMKO1E5p83w7bjvcqklp9.jpg", + "vote_average": 0, + "vote_count": 0, + "width": 1280 + }, + { + "aspect_ratio": 1.778, + "height": 1080, + "iso_639_1": "en", + "file_path": "/erkJ7OxJWFdLBOcn2MvIdhTLHTu.jpg", + "vote_average": 0, + "vote_count": 0, + "width": 1920 + } + ] + } + """ + if not tmdbinfo: + return [] + backdrops = tmdbinfo.get("images", {}).get("backdrops") or [] + result = [TMDB_IMAGE_ORIGINAL_URL % backdrop.get("file_path") for backdrop in backdrops] + result.append(TMDB_IMAGE_ORIGINAL_URL % tmdbinfo.get("backdrop_path")) + return result + + @staticmethod + def get_tmdb_season_episodes_num(tv_info, season: int): + """ + 从TMDB的季信息中获得具体季有多少集 + :param season: 季号,数字 + :param tv_info: 已获取的TMDB季的信息 + :return: 该季的总集数 + """ + if not tv_info: + return 0 + seasons = tv_info.get("seasons") + if not seasons: + return 0 + for sea in seasons: + if sea.get("season_number") == int(season): + return int(sea.get("episode_count")) + return 0 + + @staticmethod + def __dict_media_crews(crews): + """ + 字典化媒体工作人员 + """ + return [{ + "id": crew.get("id"), + "gender": crew.get("gender"), + "known_for_department": crew.get("known_for_department"), + "name": crew.get("name"), + "original_name": crew.get("original_name"), + "popularity": crew.get("popularity"), + "image": TMDB_IMAGE_FACE_URL % crew.get("profile_path"), + "credit_id": crew.get("credit_id"), + "department": crew.get("department"), + "job": crew.get("job"), + "profile": TMDB_PEOPLE_PROFILE_URL % crew.get('id') + } for crew in crews or []] + + @staticmethod + def __dict_media_casts(casts): + """ + 字典化媒体演职人员 + """ + return [{ + "id": cast.get("id"), + "gender": cast.get("gender"), + "known_for_department": cast.get("known_for_department"), + "name": cast.get("name"), + "original_name": cast.get("original_name"), + "popularity": cast.get("popularity"), + "image": TMDB_IMAGE_FACE_URL % cast.get("profile_path"), + "cast_id": cast.get("cast_id"), + "role": cast.get("character"), + "credit_id": cast.get("credit_id"), + "order": cast.get("order"), + "profile": TMDB_PEOPLE_PROFILE_URL % cast.get('id') + } for cast in casts or []] + + def get_tmdb_directors_actors(self, tmdbinfo): + """ + 查询导演和演员 + :param tmdbinfo: TMDB元数据 + :return: 导演列表,演员列表 + """ + """ + "cast": [ + { + "adult": false, + "gender": 2, + "id": 3131, + "known_for_department": "Acting", + "name": "Antonio Banderas", + "original_name": "Antonio Banderas", + "popularity": 60.896, + "profile_path": "/iWIUEwgn2KW50MssR7tdPeFoRGW.jpg", + "cast_id": 2, + "character": "Puss in Boots (voice)", + "credit_id": "6052480e197de4006bb47b9a", + "order": 0 + } + ], + "crew": [ + { + "adult": false, + "gender": 2, + "id": 5524, + "known_for_department": "Production", + "name": "Andrew Adamson", + "original_name": "Andrew Adamson", + "popularity": 9.322, + "profile_path": "/qqIAVKAe5LHRbPyZUlptsqlo4Kb.jpg", + "credit_id": "63b86b2224b33300a0585bf1", + "department": "Production", + "job": "Executive Producer" + } + ] + """ + if not tmdbinfo: + return [], [] + _credits = tmdbinfo.get("credits") + if not _credits: + return [], [] + directors = [] + actors = [] + for cast in self.__dict_media_casts(_credits.get("cast")): + if cast.get("known_for_department") == "Acting": + actors.append(cast) + for crew in self.__dict_media_crews(_credits.get("crew")): + if crew.get("job") == "Director": + directors.append(crew) + return directors, actors + + def get_tmdb_cats(self, mtype, tmdbid): + """ + 获取TMDB的演员列表 + :param: mtype: 媒体类型 + :param: tmdbid: TMDBID + """ + try: + if mtype == MediaType.MOVIE: + if not self.movie: + return [] + return self.__dict_media_casts(self.movie.credits(tmdbid).get("cast")) + else: + if not self.tv: + return [] + return self.__dict_media_casts(self.tv.credits(tmdbid).get("cast")) + except Exception as err: + print(str(err)) + return [] + + @staticmethod + def get_tmdb_genres_names(tmdbinfo): + """ + 从TMDB数据中获取风格名称 + """ + """ + "genres": [ + { + "id": 16, + "name": "动画" + }, + { + "id": 28, + "name": "动作" + }, + { + "id": 12, + "name": "冒险" + }, + { + "id": 35, + "name": "喜剧" + }, + { + "id": 10751, + "name": "家庭" + }, + { + "id": 14, + "name": "奇幻" + } + ] + """ + if not tmdbinfo: + return "" + genres = tmdbinfo.get("genres") or [] + genres_list = [genre.get("name") for genre in genres] + return ", ".join(genres_list) if genres_list else "" + + def get_tmdb_genres(self, mtype): + """ + 获取TMDB的风格列表 + :param: mtype: 媒体类型 + """ + if not self.genre: + return [] + try: + if mtype == MediaType.MOVIE: + return self.genre.movie_list() + else: + return self.genre.tv_list() + except Exception as err: + print(str(err)) + return [] + + @staticmethod + def get_get_production_country_names(tmdbinfo): + """ + 从TMDB数据中获取制片国家名称 + """ + """ + "production_countries": [ + { + "iso_3166_1": "US", + "name": "美国" + } + ] + """ + if not tmdbinfo: + return "" + countries = tmdbinfo.get("production_countries") or [] + countries_list = [country.get("name") for country in countries] + return ", ".join(countries_list) if countries_list else "" + + @staticmethod + def get_tmdb_production_company_names(tmdbinfo): + """ + 从TMDB数据中获取制片公司名称 + """ + """ + "production_companies": [ + { + "id": 2, + "logo_path": "/wdrCwmRnLFJhEoH8GSfymY85KHT.png", + "name": "DreamWorks Animation", + "origin_country": "US" + } + ] + """ + if not tmdbinfo: + return "" + companies = tmdbinfo.get("production_companies") or [] + companies_list = [company.get("name") for company in companies] + return ", ".join(companies_list) if companies_list else "" + + @staticmethod + def get_tmdb_crews(tmdbinfo, nums=None): + """ + 从TMDB数据中获取制片人员 + """ + if not tmdbinfo: + return "" + crews = tmdbinfo.get("credits", {}).get("crew") or [] + result = [{crew.get("name"): crew.get("job")} for crew in crews] + if nums: + return result[:nums] + else: + return result + + def get_tmdb_en_title(self, media_info): + """ + 获取TMDB的英文名称 + """ + en_info = self.get_tmdb_info(mtype=media_info.type, + tmdbid=media_info.tmdb_id, + language="en-US") + if en_info: + return en_info.get("title") if media_info.type == MediaType.MOVIE else en_info.get("name") + return None + + def get_episode_title(self, media_info): + """ + 获取剧集的标题 + """ + if media_info.type == MediaType.MOVIE: + return None + if media_info.tmdb_id: + if not media_info.begin_episode: + return None + episodes = self.get_tmdb_season_episodes(tmdbid=media_info.tmdb_id, + season=int(media_info.get_season_seq())) + for episode in episodes: + if episode.get("episode_number") == media_info.begin_episode: + return episode.get("name") + return None + + def get_movie_similar(self, tmdbid, page=1): + """ + 查询类似电影 + """ + if not self.movie: + return [] + try: + movies = self.movie.similar(movie_id=tmdbid, page=page) or [] + return self.__dict_tmdbinfos(movies, MediaType.MOVIE) + except Exception as e: + print(str(e)) + return [] + + def get_movie_recommendations(self, tmdbid, page=1): + """ + 查询电影关联推荐 + """ + if not self.movie: + return [] + try: + movies = self.movie.recommendations(movie_id=tmdbid, page=page) or [] + return self.__dict_tmdbinfos(movies, MediaType.MOVIE) + except Exception as e: + print(str(e)) + return [] + + def get_tv_similar(self, tmdbid, page=1): + """ + 查询类似电视剧 + """ + if not self.tv: + return [] + try: + tvs = self.tv.similar(tv_id=tmdbid, page=page) or [] + return self.__dict_tmdbinfos(tvs, MediaType.TV) + except Exception as e: + print(str(e)) + return [] + + def get_tv_recommendations(self, tmdbid, page=1): + """ + 查询电视剧关联推荐 + """ + if not self.tv: + return [] + try: + tvs = self.tv.recommendations(tv_id=tmdbid, page=page) or [] + return self.__dict_tmdbinfos(tvs, MediaType.TV) + except Exception as e: + print(str(e)) + return [] + + def get_tmdb_discover(self, mtype, params=None, page=1): + """ + 浏览电影、电视剧(复杂过滤条件) + """ + if not self.discover: + return [] + try: + if mtype == MediaType.MOVIE: + movies = self.discover.discover_movies(params=params, page=page) + return self.__dict_tmdbinfos(movies, mtype) + elif mtype == MediaType.TV: + tvs = self.discover.discover_tv_shows(params=params, page=page) + return self.__dict_tmdbinfos(tvs, mtype) + except Exception as e: + print(str(e)) + return [] + + def get_person_medias(self, personid, mtype, page=1): + """ + 查询人物相关影视作品 + """ + if not self.person: + return [] + result = [] + try: + if mtype == MediaType.MOVIE: + movies = self.person.movie_credits(person_id=personid) or [] + result = self.__dict_tmdbinfos(movies, mtype) + elif mtype == MediaType.TV: + tvs = self.person.tv_credits(person_id=personid) or [] + result = self.__dict_tmdbinfos(tvs, mtype) + return result[(page - 1) * 20: page * 20] + except Exception as e: + print(str(e)) + return [] + + @staticmethod + def __search_engine(feature_name): + """ + 辅助识别关键字 + """ + is_movie = False + if not feature_name: + return None, is_movie + # 剔除不必要字符 + feature_name = re.compile(r"^\w+字幕[组社]?", re.IGNORECASE).sub("", feature_name) + backlist = sorted(KEYWORD_BLACKLIST, key=lambda x: len(x), reverse=True) + for single in backlist: + feature_name = feature_name.replace(single, " ") + if not feature_name: + return None, is_movie + + def cal_score(strongs, r_dict): + for i, s in enumerate(strongs): + if len(strongs) < 5: + if i < 2: + score = KEYWORD_SEARCH_WEIGHT_3[0] + else: + score = KEYWORD_SEARCH_WEIGHT_3[1] + elif len(strongs) < 10: + if i < 2: + score = KEYWORD_SEARCH_WEIGHT_2[0] + else: + score = KEYWORD_SEARCH_WEIGHT_2[1] if i < (len(strongs) >> 1) else KEYWORD_SEARCH_WEIGHT_2[2] + else: + if i < 2: + score = KEYWORD_SEARCH_WEIGHT_1[0] + else: + score = KEYWORD_SEARCH_WEIGHT_1[1] if i < (len(strongs) >> 2) else KEYWORD_SEARCH_WEIGHT_1[ + 2] if i < ( + len(strongs) >> 1) \ + else KEYWORD_SEARCH_WEIGHT_1[3] if i < (len(strongs) >> 2 + len(strongs) >> 1) else \ + KEYWORD_SEARCH_WEIGHT_1[ + 4] + if r_dict.__contains__(s.lower()): + r_dict[s.lower()] += score + continue + r_dict[s.lower()] = score + + bing_url = "https://www.cn.bing.com/search?q=%s&qs=n&form=QBRE&sp=-1" % feature_name + baidu_url = "https://www.baidu.com/s?ie=utf-8&tn=baiduhome_pg&wd=%s" % feature_name + res_bing = RequestUtils(timeout=5).get_res(url=bing_url) + res_baidu = RequestUtils(timeout=5).get_res(url=baidu_url) + ret_dict = {} + if res_bing and res_bing.status_code == 200: + html_text = res_bing.text + if html_text: + html = etree.HTML(html_text) + strongs_bing = list( + filter(lambda x: (0 if not x else difflib.SequenceMatcher(None, feature_name, + x).ratio()) > KEYWORD_STR_SIMILARITY_THRESHOLD, + map(lambda x: x.text, html.cssselect( + "#sp_requery strong, #sp_recourse strong, #tile_link_cn strong, .b_ad .ad_esltitle~div strong, h2 strong, .b_caption p strong, .b_snippetBigText strong, .recommendationsTableTitle+.b_slideexp strong, .recommendationsTableTitle+table strong, .recommendationsTableTitle+ul strong, .pageRecoContainer .b_module_expansion_control strong, .pageRecoContainer .b_title>strong, .b_rs strong, .b_rrsr strong, #dict_ans strong, .b_listnav>.b_ans_stamp>strong, #b_content #ans_nws .na_cnt strong, .adltwrnmsg strong")))) + if strongs_bing: + title = html.xpath("//aside//h2[@class = \" b_entityTitle\"]/text()") + if len(title) > 0: + if title: + t = re.compile(r"\s*\(\d{4}\)$").sub("", title[0]) + ret_dict[t] = 200 + if html.xpath("//aside//div[@data-feedbk-ids = \"Movie\"]"): + is_movie = True + cal_score(strongs_bing, ret_dict) + if res_baidu and res_baidu.status_code == 200: + html_text = res_baidu.text + if html_text: + html = etree.HTML(html_text) + ems = list( + filter(lambda x: (0 if not x else difflib.SequenceMatcher(None, feature_name, + x).ratio()) > KEYWORD_STR_SIMILARITY_THRESHOLD, + map(lambda x: x.text, html.cssselect("em")))) + if len(ems) > 0: + cal_score(ems, ret_dict) + if not ret_dict: + return None, False + ret = sorted(ret_dict.items(), key=lambda d: d[1], reverse=True) + log.info("【Meta】推断关键字为:%s ..." % ([k[0] for i, k in enumerate(ret) if i < 4])) + if len(ret) == 1: + keyword = ret[0][0] + else: + pre = ret[0] + nextw = ret[1] + if nextw[0].find(pre[0]) > -1: + # 满分直接判定 + if int(pre[1]) >= 100: + keyword = pre[0] + # 得分相差30 以上, 选分高 + elif int(pre[1]) - int(nextw[1]) > KEYWORD_DIFF_SCORE_THRESHOLD: + keyword = pre[0] + # 重复的不选 + elif nextw[0].replace(pre[0], "").strip() == pre[0]: + keyword = pre[0] + # 纯数字不选 + elif pre[0].isdigit(): + keyword = nextw[0] + else: + keyword = nextw[0] + + else: + keyword = pre[0] + log.info("【Meta】选择关键字为:%s " % keyword) + return keyword, is_movie + + @staticmethod + def __get_genre_ids_from_detail(genres): + """ + 从TMDB详情中获取genre_id列表 + """ + if not genres: + return [] + genre_ids = [] + for genre in genres: + genre_ids.append(genre.get('id')) + return genre_ids + + @staticmethod + def __get_tmdb_chinese_title(tmdbinfo): + """ + 从别名中获取中文标题 + """ + if not tmdbinfo: + return None + if tmdbinfo.get("media_type") == MediaType.MOVIE: + alternative_titles = tmdbinfo.get("alternative_titles", {}).get("titles", []) + else: + alternative_titles = tmdbinfo.get("alternative_titles", {}).get("results", []) + for alternative_title in alternative_titles: + iso_3166_1 = alternative_title.get("iso_3166_1") + if iso_3166_1 == "CN": + title = alternative_title.get("title") + if title and StringUtils.is_chinese(title) and zhconv.convert(title, "zh-hans") == title: + return title + return tmdbinfo.get("title") if tmdbinfo.get("media_type") == MediaType.MOVIE else tmdbinfo.get("name") + + def get_tmdbperson_chinese_name(self, person_id): + """ + 查询TMDB人物中文名称 + """ + if not self.person: + return "" + alter_names = [] + name = "" + try: + aka_names = self.person.details(person_id).get("also_known_as", []) or [] + except Exception as err: + print(str(err)) + return "" + for aka_name in aka_names: + if StringUtils.is_chinese(aka_name): + alter_names.append(aka_name) + if len(alter_names) == 1: + name = alter_names[0] + elif len(alter_names) > 1: + for alter_name in alter_names: + if alter_name == zhconv.convert(alter_name, 'zh-hans'): + name = alter_name + return name + + def get_tmdbperson_aka_names(self, person_id): + """ + 查询人物又名 + """ + if not self.person: + return [] + try: + aka_names = self.person.details(person_id).get("also_known_as", []) or [] + return aka_names + except Exception as err: + print(str(err)) + return [] + + def get_random_discover_backdrop(self): + """ + 获取TMDB热门电影随机一张背景图 + """ + try: + # 随机类型 + mtype = MediaType.MOVIE if random.uniform(0, 1) > 0.5 else MediaType.TV + # 热门电影/电视剧 + if mtype == MediaType.MOVIE: + medias = self.discover.discover_movies(params={"sort_by": "popularity.desc"}) + else: + medias = self.discover.discover_tv_shows(params={"sort_by": "popularity.desc"}) + if medias: + backdrops = [media.get("backdrop_path") for media in medias if media.get("backdrop_path")] + # 随机一张 + return TMDB_IMAGE_ORIGINAL_URL % backdrops[round(random.uniform(0, len(backdrops) - 1))] + except Exception as err: + print(str(err)) + return "" + + def save_rename_cache(self, file_name, cache_info): + """ + 将手动识别的信息加入缓存 + """ + if not file_name or not cache_info: + return + meta_info = MetaInfo(title=file_name) + self.__insert_media_cache(self.__make_cache_key(meta_info), cache_info) + + @staticmethod + def merge_media_info(target, source): + """ + 将soruce中有效的信息合并到target中并返回 + """ + target.set_tmdb_info(source.tmdb_info) + target.fanart_poster = source.get_poster_image() + target.fanart_backdrop = source.get_backdrop_image() + target.set_download_info(download_setting=source.download_setting, + save_path=source.save_path) + return target + + def get_tmdbid_by_imdbid(self, imdbid): + """ + 根据IMDBID查询TMDB信息 + """ + if not self.find: + return None + try: + result = self.find.find_by_imdbid(imdbid) or {} + tmdbinfo = result.get('movie_results') or result.get("tv_results") + if tmdbinfo: + tmdbinfo = tmdbinfo[0] + return tmdbinfo.get("id") + except Exception as err: + print(str(err)) + return None + + @staticmethod + def get_detail_url(mtype, tmdbid): + """ + 获取TMDB/豆瓣详情页地址 + """ + if not tmdbid: + return "" + if str(tmdbid).startswith("DB:"): + return "https://movie.douban.com/subject/%s" % str(tmdbid).replace("DB:", "") + elif mtype == MediaType.MOVIE: + return "https://www.themoviedb.org/movie/%s" % tmdbid + else: + return "https://www.themoviedb.org/tv/%s" % tmdbid + + def get_episode_images(self, tv_id, season_id, episode_id, orginal=False): + """ + 获取剧集中某一集封面 + """ + if not self.episode: + return "" + res = self.episode.images(tv_id, season_id, episode_id) + if res: + if orginal: + return TMDB_IMAGE_ORIGINAL_URL % res[0].get("file_path") + else: + return TMDB_IMAGE_W500_URL % res[0].get("file_path") + else: + return "" + + def get_tmdb_factinfo(self, media_info): + """ + 获取TMDB发布信息 + """ + result = [] + if media_info.vote_average: + result.append({"评分": media_info.vote_average}) + if media_info.original_title: + result.append({"原始标题": media_info.original_title}) + status = media_info.tmdb_info.get("status") + if status: + result.append({"状态": status}) + if media_info.release_date: + result.append({"上映日期": media_info.release_date}) + revenue = media_info.tmdb_info.get("revenue") + if revenue: + result.append({"收入": StringUtils.str_amount(revenue)}) + budget = media_info.tmdb_info.get("budget") + if media_info.vote_average: + result.append({"成本": StringUtils.str_amount(budget)}) + if budget: + result.append({"原始语言": media_info.original_language}) + production_country = self.get_get_production_country_names(tmdbinfo=media_info.tmdb_info) + if production_country: + result.append({"出品国家": production_country}), + production_company = self.get_tmdb_production_company_names(tmdbinfo=media_info.tmdb_info) + if production_company: + result.append({"制作公司": production_company}) + + return result diff --git a/app/media/meta/__init__.py b/app/media/meta/__init__.py new file mode 100644 index 0000000..a317292 --- /dev/null +++ b/app/media/meta/__init__.py @@ -0,0 +1,5 @@ +from .metainfo import MetaInfo +from .metaanime import MetaAnime +from ._base import MetaBase +from .metavideo import MetaVideo +from .release_groups import ReleaseGroupsMatcher diff --git a/app/media/meta/_base.py b/app/media/meta/_base.py new file mode 100644 index 0000000..156ebf8 --- /dev/null +++ b/app/media/meta/_base.py @@ -0,0 +1,710 @@ +import re +import cn2an +from app.media.fanart import Fanart +from config import ANIME_GENREIDS, DEFAULT_TMDB_IMAGE, TMDB_IMAGE_W500_URL +from app.media.category import Category +from app.utils import StringUtils, ExceptionUtils +from app.utils.types import MediaType + + +class MetaBase(object): + """ + 媒体信息基类 + """ + proxies = None + category_handler = None + # 是否处理的文件 + fileflag = False + # 原字符串 + org_string = None + # 副标题 + subtitle = None + # 类型 电影、电视剧 + type = None + # 识别的中文名 + cn_name = None + # 识别的英文名 + en_name = None + # 总季数 + total_seasons = 0 + # 识别的开始季 数字 + begin_season = None + # 识别的结束季 数字 + end_season = None + # 总集数 + total_episodes = 0 + # 识别的开始集 + begin_episode = None + # 识别的结束集 + end_episode = None + # Partx Cd Dvd Disk Disc + part = None + # 识别的资源类型 + resource_type = None + # 识别的效果 + resource_effect = None + # 识别的分辨率 + resource_pix = None + # 识别的制作组/字幕组 + resource_team = None + # 视频编码 + video_encode = None + # 音频编码 + audio_encode = None + # 二级分类 + category = None + # TMDB ID + tmdb_id = 0 + # IMDB ID + imdb_id = "" + # TVDB ID + tvdb_id = 0 + # 豆瓣 ID + douban_id = 0 + # 自定义搜索词 + keyword = None + # 媒体标题 + title = None + # 媒体原语种 + original_language = None + # 媒体原发行标题 + original_title = None + # 媒体发行日期 + release_date = None + # 播放时长 + runtime = 0 + # 媒体年份 + year = None + # 封面图片 + backdrop_path = None + poster_path = None + fanart_backdrop = None + fanart_poster = None + # 评分 + vote_average = 0 + # 描述 + overview = None + # TMDB 的其它信息 + tmdb_info = {} + # 本地状态 1-已订阅 2-已存在 + fav = "0" + # 站点列表 + rss_sites = [] + search_sites = [] + # 种子附加信息 + # 站点名称 + site = None + # 站点优先级 + site_order = 0 + # 操作用户 + user_name = None + # 种子链接 + enclosure = None + # 资源优先级 + res_order = 0 + # 使用的过滤规则 + filter_rule = None + # 是否洗版 + over_edition = None + # 种子大小 + size = 0 + # 做种者 + seeders = 0 + # 下载者 + peers = 0 + # 种子描述 + description = None + # 详情页面 + page_url = None + # 上传因子 + upload_volume_factor = None + # 下载因子 + download_volume_factor = None + # HR + hit_and_run = None + # 订阅ID + rssid = None + # 保存目录 + save_path = None + # 下载设置 + download_setting = None + # 识别辅助 + ignored_words = None + replaced_words = None + offset_words = None + # 备注字典 + note = {} + # 副标题解析 + _subtitle_flag = False + _subtitle_season_re = r"[第\s]+([0-9一二三四五六七八九十S\-]+)\s*季" + _subtitle_season_all_re = r"全\s*([0-9一二三四五六七八九十]+)\s*季|([0-9一二三四五六七八九十]+)\s*季全" + _subtitle_episode_re = r"[第\s]+([0-9一二三四五六七八九十EP\-]+)\s*[集话話期]" + _subtitle_episode_all_re = r"([0-9一二三四五六七八九十]+)\s*集全|全\s*([0-9一二三四五六七八九十]+)\s*[集话話期]" + + def __init__(self, title, subtitle=None, fileflag=False): + self.category_handler = Category() + self.fanart = Fanart() + if not title: + return + self.org_string = title + self.subtitle = subtitle + self.fileflag = fileflag + + def get_name(self): + if self.cn_name and StringUtils.is_all_chinese(self.cn_name): + return self.cn_name + elif self.en_name: + return self.en_name + elif self.cn_name: + return self.cn_name + return "" + + def get_title_string(self): + if self.title: + return "%s (%s)" % (self.title, self.year) if self.year else self.title + elif self.get_name(): + return "%s (%s)" % (self.get_name(), self.year) if self.year else self.get_name() + else: + return "" + + def get_star_string(self): + if self.vote_average: + return "评分:%s" % self.get_stars() + else: + return "" + + def get_vote_string(self): + if self.vote_average: + return "评分:%s" % round(float(self.vote_average), 1) + else: + return "" + + def get_type_string(self): + if not self.type: + return "" + return "类型:%s" % self.type.value + + def get_title_vote_string(self): + if not self.vote_average: + return self.get_title_string() + else: + return "%s\n%s" % (self.get_title_string(), self.get_star_string()) + + def get_title_ep_string(self): + string = self.get_title_string() + if self.get_episode_list(): + string = "%s %s" % (string, self.get_season_episode_string()) + else: + if self.get_season_list(): + string = "%s %s" % (string, self.get_season_string()) + return string + + def get_overview_string(self, max_len=140): + """ + 返回带限定长度的简介信息 + :param max_len: 内容长度 + :return: + """ + if not hasattr(self, "overview"): + return "" + + overview = str(self.overview).strip() + placeholder = ' ...' + max_len = max(len(placeholder), max_len - len(placeholder)) + overview = (overview[:max_len] + placeholder) if len(overview) > max_len else overview + return overview + + # 返回季字符串 + def get_season_string(self): + if self.begin_season is not None: + return "S%s" % str(self.begin_season).rjust(2, "0") \ + if self.end_season is None \ + else "S%s-S%s" % \ + (str(self.begin_season).rjust(2, "0"), + str(self.end_season).rjust(2, "0")) + else: + if self.type == MediaType.MOVIE: + return "" + else: + return "S01" + + # 返回begin_season 的Sxx + def get_season_item(self): + if self.begin_season is not None: + return "S%s" % str(self.begin_season).rjust(2, "0") + else: + if self.type == MediaType.MOVIE: + return "" + else: + return "S01" + + # 返回begin_season 的数字 + def get_season_seq(self): + if self.begin_season is not None: + return str(self.begin_season) + else: + if self.type == MediaType.MOVIE: + return "" + else: + return "1" + + # 返回季的数组 + def get_season_list(self): + if self.begin_season is None: + if self.type == MediaType.MOVIE: + return [] + else: + return [1] + elif self.end_season is not None: + return [season for season in range(self.begin_season, self.end_season + 1)] + else: + return [self.begin_season] + + # 返回集字符串 + def get_episode_string(self): + if self.begin_episode is not None: + return "E%s" % str(self.begin_episode).rjust(2, "0") \ + if self.end_episode is None \ + else "E%s-E%s" % \ + ( + str(self.begin_episode).rjust(2, "0"), + str(self.end_episode).rjust(2, "0")) + else: + return "" + + # 返回集的数组 + def get_episode_list(self): + if self.begin_episode is None: + return [] + elif self.end_episode is not None: + return [episode for episode in range(self.begin_episode, self.end_episode + 1)] + else: + return [self.begin_episode] + + # 返回集的并列表达方式,用于支持单文件多集 + def get_episode_items(self): + return "E%s" % "E".join(str(episode).rjust(2, '0') for episode in self.get_episode_list()) + + # 返回单文件多集的集数表达方式,用于支持单文件多集 + def get_episode_seqs(self): + episodes = self.get_episode_list() + if episodes: + # 集 xx + if len(episodes) == 1: + return str(episodes[0]) + else: + return "%s-%s" % (episodes[0], episodes[-1]) + else: + return "" + + # 返回begin_episode 的数字 + def get_episode_seq(self): + episodes = self.get_episode_list() + if episodes: + return str(episodes[0]) + else: + return "" + + # 返回季集字符串 + def get_season_episode_string(self): + if self.type == MediaType.MOVIE: + return "" + else: + seaion = self.get_season_string() + episode = self.get_episode_string() + if seaion and episode: + return "%s %s" % (seaion, episode) + elif seaion: + return "%s" % seaion + elif episode: + return "%s" % episode + return "" + + # 返回资源类型字符串,含分辨率 + def get_resource_type_string(self): + ret_string = "" + if self.resource_type: + ret_string = f"{ret_string} {self.resource_type}" + if self.resource_effect: + ret_string = f"{ret_string} {self.resource_effect}" + if self.resource_pix: + ret_string = f"{ret_string} {self.resource_pix}" + return ret_string + + # 返回资源类型字符串,不含分辨率 + def get_edtion_string(self): + ret_string = "" + if self.resource_type: + ret_string = f"{ret_string} {self.resource_type}" + if self.resource_effect: + ret_string = f"{ret_string} {self.resource_effect}" + return ret_string.strip() + + # 返回发布组/字幕组字符串 + def get_resource_team_string(self): + if self.resource_team: + return self.resource_team + else: + return "" + + # 返回视频编码 + def get_video_encode_string(self): + return self.video_encode or "" + + # 返回音频编码 + def get_audio_encode_string(self): + return self.audio_encode or "" + + # 返回背景图片地址 + def get_backdrop_image(self, default=True, original=False): + if self.fanart_backdrop: + return self.fanart_backdrop + else: + self.fanart_backdrop = self.fanart.get_backdrop(media_type=self.type, + queryid=self.tmdb_id if self.type == MediaType.MOVIE else self.tvdb_id) + if self.fanart_backdrop: + return self.fanart_backdrop + elif self.backdrop_path: + if original: + return self.backdrop_path.replace("/w500", "/original") + else: + return self.backdrop_path + else: + return "../static/img/tmdb.webp" if default else "" + + # 返回消息图片地址 + def get_message_image(self): + if self.fanart_backdrop: + return self.fanart_backdrop + else: + self.fanart_backdrop = self.fanart.get_backdrop(media_type=self.type, + queryid=self.tmdb_id if self.type == MediaType.MOVIE else self.tvdb_id) + if self.fanart_backdrop: + return self.fanart_backdrop + elif self.backdrop_path: + return self.backdrop_path + elif self.poster_path: + return self.poster_path + else: + return DEFAULT_TMDB_IMAGE + + # 返回海报图片地址 + def get_poster_image(self, original=False): + if self.poster_path: + if original: + return self.poster_path.replace("/w500", "/original") + else: + return self.poster_path + if not self.fanart_poster: + self.fanart_poster = self.fanart.get_poster(media_type=self.type, + queryid=self.tmdb_id if self.type == MediaType.MOVIE else self.tvdb_id) + return self.fanart_poster or "" + + # 查询TMDB详情页URL + def get_detail_url(self): + if self.tmdb_id: + if str(self.tmdb_id).startswith("DB:"): + return "https://movie.douban.com/subject/%s" % str(self.tmdb_id).replace("DB:", "") + elif self.type == MediaType.MOVIE: + return "https://www.themoviedb.org/movie/%s" % self.tmdb_id + else: + return "https://www.themoviedb.org/tv/%s" % self.tmdb_id + elif self.douban_id: + return "https://movie.douban.com/subject/%s" % self.douban_id + return "" + + def get_douban_detail_url(self): + if self.douban_id: + return "https://movie.douban.com/subject/%s" % self.douban_id + return "" + + # 返回评分星星个数 + def get_stars(self): + if not self.vote_average: + return "" + return "".rjust(int(self.vote_average), "★") + + # 返回促销信息 + def get_volume_factor_string(self): + return self.get_free_string(self.upload_volume_factor, self.download_volume_factor) + + @staticmethod + def get_free_string(upload_volume_factor, download_volume_factor): + if upload_volume_factor is None or download_volume_factor is None: + return "未知" + free_strs = { + "1.0 1.0": "普通", + "1.0 0.0": "免费", + "2.0 1.0": "2X", + "2.0 0.0": "2X免费", + "1.0 0.5": "50%", + "2.0 0.5": "2X 50%", + "1.0 0.7": "70%", + "1.0 0.3": "30%" + } + return free_strs.get('%.1f %.1f' % (upload_volume_factor, download_volume_factor), "未知") + + # 是否包含季 + def is_in_season(self, season): + if isinstance(season, list): + if self.end_season is not None: + meta_season = list(range(self.begin_season, self.end_season + 1)) + else: + if self.begin_season is not None: + meta_season = [self.begin_season] + else: + meta_season = [1] + + return set(meta_season).issuperset(set(season)) + else: + if self.end_season is not None: + return self.begin_season <= int(season) <= self.end_season + else: + if self.begin_season is not None: + return int(season) == self.begin_season + else: + return int(season) == 1 + + # 是否包含集 + def is_in_episode(self, episode): + if isinstance(episode, list): + if self.end_episode is not None: + meta_episode = list(range(self.begin_episode, self.end_episode + 1)) + else: + meta_episode = [self.begin_episode] + return set(meta_episode).issuperset(set(episode)) + else: + if self.end_episode is not None: + return self.begin_episode <= int(episode) <= self.end_episode + else: + return int(episode) == self.begin_episode + + # 整合TMDB识别的信息 + def set_tmdb_info(self, info): + if not info: + return + self.type = self.__get_tmdb_type(info) + if not self.type: + return + self.tmdb_id = info.get('id') + if not self.tmdb_id: + return + if info.get("external_ids"): + self.tvdb_id = info.get("external_ids", {}).get("tvdb_id", 0) + self.imdb_id = info.get("external_ids", {}).get("imdb_id", "") + self.tmdb_info = info + self.vote_average = round(float(info.get('vote_average')), 1) if info.get('vote_average') else 0 + self.overview = info.get('overview') + if self.type == MediaType.MOVIE: + self.title = info.get('title') + self.original_title = info.get('original_title') + self.original_language = info.get('original_language') + self.runtime = info.get("runtime") + self.release_date = info.get('release_date') + if self.release_date: + self.year = self.release_date[0:4] + self.category = self.category_handler.get_movie_category(info) + else: + self.title = info.get('name') + self.original_title = info.get('original_name') + self.original_language = info.get('original_language') + self.runtime = info.get("episode_run_time")[0] if info.get("episode_run_time") else None + self.release_date = info.get('first_air_date') + if self.release_date: + self.year = self.release_date[0:4] + if self.type == MediaType.TV: + self.category = self.category_handler.get_tv_category(info) + else: + self.category = self.category_handler.get_anime_category(info) + self.poster_path = TMDB_IMAGE_W500_URL % info.get('poster_path') if info.get( + 'poster_path') else "" + self.backdrop_path = TMDB_IMAGE_W500_URL % info.get('backdrop_path') if info.get( + 'backdrop_path') else "" + + # 整合种了信息 + def set_torrent_info(self, + site=None, + site_order=0, + enclosure=None, + res_order=0, + filter_rule=None, + size=0, + seeders=0, + peers=0, + description=None, + page_url=None, + upload_volume_factor=None, + download_volume_factor=None, + rssid=None, + hit_and_run=None, + imdbid=None, + over_edition=None): + if site: + self.site = site + if site_order: + self.site_order = site_order + if enclosure: + self.enclosure = enclosure + if res_order: + self.res_order = res_order + if filter_rule: + self.filter_rule = filter_rule + if size: + self.size = size + if seeders: + self.seeders = seeders + if peers: + self.peers = peers + if description: + self.description = description + if page_url: + self.page_url = page_url + if upload_volume_factor is not None: + self.upload_volume_factor = upload_volume_factor + if download_volume_factor is not None: + self.download_volume_factor = download_volume_factor + if rssid: + self.rssid = rssid + if hit_and_run is not None: + self.hit_and_run = hit_and_run + if imdbid is not None: + self.imdb_id = imdbid + if over_edition is not None: + self.over_edition = over_edition + + # 整合下载参数 + def set_download_info(self, download_setting=None, save_path=None): + if download_setting: + self.download_setting = download_setting + if save_path: + self.save_path = save_path + + # 判断电视剧是否为动漫 + def __get_tmdb_type(self, info): + if not info: + return self.type + if not info.get('media_type'): + return self.type + if info.get('media_type') == MediaType.TV: + genre_ids = info.get("genre_ids") + if not genre_ids: + return MediaType.TV + if isinstance(genre_ids, list): + genre_ids = [str(val).upper() for val in genre_ids] + else: + genre_ids = [str(genre_ids).upper()] + if set(genre_ids).intersection(set(ANIME_GENREIDS)): + return MediaType.ANIME + else: + return MediaType.TV + else: + return info.get('media_type') + + def init_subtitle(self, title_text): + if not title_text: + return + if re.search(r'[全第季集话話期]', title_text, re.IGNORECASE): + # 第x季 + season_str = re.search(r'%s' % self._subtitle_season_re, title_text, re.IGNORECASE) + if season_str: + seasons = season_str.group(1) + if seasons: + seasons = seasons.upper().replace("S", "").strip() + else: + return + try: + end_season = None + if seasons.find('-') != -1: + seasons = seasons.split('-') + begin_season = int(cn2an.cn2an(seasons[0].strip(), mode='smart')) + if len(seasons) > 1: + end_season = int(cn2an.cn2an(seasons[1].strip(), mode='smart')) + else: + begin_season = int(cn2an.cn2an(seasons, mode='smart')) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return + if self.begin_season is None and isinstance(begin_season, int): + self.begin_season = begin_season + self.total_seasons = 1 + if self.begin_season is not None \ + and self.end_season is None \ + and isinstance(end_season, int) \ + and end_season != self.begin_season: + self.end_season = end_season + self.total_seasons = (self.end_season - self.begin_season) + 1 + self.type = MediaType.TV + self._subtitle_flag = True + # 第x集 + episode_str = re.search(r'%s' % self._subtitle_episode_re, title_text, re.IGNORECASE) + if episode_str: + episodes = episode_str.group(1) + if episodes: + episodes = episodes.upper().replace("E", "").replace("P", "").strip() + else: + return + try: + end_episode = None + if episodes.find('-') != -1: + episodes = episodes.split('-') + begin_episode = int(cn2an.cn2an(episodes[0].strip(), mode='smart')) + if len(episodes) > 1: + end_episode = int(cn2an.cn2an(episodes[1].strip(), mode='smart')) + else: + begin_episode = int(cn2an.cn2an(episodes, mode='smart')) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return + if self.begin_episode is None and isinstance(begin_episode, int): + self.begin_episode = begin_episode + self.total_episodes = 1 + if self.begin_episode is not None \ + and self.end_episode is None \ + and isinstance(end_episode, int) \ + and end_episode != self.begin_episode: + self.end_episode = end_episode + self.total_episodes = (self.end_episode - self.begin_episode) + 1 + self.type = MediaType.TV + self._subtitle_flag = True + # x集全 + episode_all_str = re.search(r'%s' % self._subtitle_episode_all_re, title_text, re.IGNORECASE) + if episode_all_str: + self.begin_episode = None + self.end_episode = None + self.total_episodes = 0 + self.type = MediaType.TV + # 全x季 x季全 + season_all_str = re.search(r"%s" % self._subtitle_season_all_re, title_text, re.IGNORECASE) + if season_all_str: + season_all = season_all_str.group(1) + if not season_all: + season_all = season_all_str.group(2) + if season_all and self.begin_season is None and self.begin_episode is None: + try: + self.total_seasons = int(cn2an.cn2an(season_all.strip(), mode='smart')) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return + self.begin_season = 1 + self.end_season = self.total_seasons + self.type = MediaType.TV + self._subtitle_flag = True + + def to_dict(self): + """ + 转化为字典 + """ + return { + "id": self.tmdb_id, + 'orgid': self.tmdb_id, + "title": self.title, + "year": self.year, + "type": self.type.value if self.type else "", + "media_type": self.type.value if self.type else "", + 'vote': self.vote_average, + 'image': self.poster_path, + "imdb_id": self.imdb_id, + "tmdb_id": self.tmdb_id, + "overview": str(self.overview).strip() if self.overview else '', + "link": self.get_detail_url() + } diff --git a/app/media/meta/metaanime.py b/app/media/meta/metaanime.py new file mode 100644 index 0000000..a535971 --- /dev/null +++ b/app/media/meta/metaanime.py @@ -0,0 +1,220 @@ +import re + +import zhconv + +import anitopy +from app.media.meta._base import MetaBase +from app.media.meta.release_groups import ReleaseGroupsMatcher +from app.utils import StringUtils, ExceptionUtils +from app.utils.types import MediaType + + +class MetaAnime(MetaBase): + """ + 识别动漫 + """ + _anime_no_words = ['CHS&CHT', 'MP4', 'GB MP4', 'WEB-DL'] + _name_nostring_re = r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}" + + def __init__(self, title, subtitle=None, fileflag=False): + super().__init__(title, subtitle, fileflag) + if not title: + return + # 调用第三方模块识别动漫 + try: + original_title = title + # 字幕组信息会被预处理掉 + anitopy_info_origin = anitopy.parse(title) + title = self.__prepare_title(title) + anitopy_info = anitopy.parse(title) + if anitopy_info: + # 名称 + name = anitopy_info.get("anime_title") + if name and name.find("/") != -1: + name = name.split("/")[-1].strip() + if not name or name in self._anime_no_words or (len(name) < 5 and not StringUtils.is_chinese(name)): + anitopy_info = anitopy.parse("[ANIME]" + title) + if anitopy_info: + name = anitopy_info.get("anime_title") + if not name or name in self._anime_no_words or (len(name) < 5 and not StringUtils.is_chinese(name)): + name_match = re.search(r'\[(.+?)]', title) + if name_match and name_match.group(1): + name = name_match.group(1).strip() + # 拆份中英文名称 + if name: + lastword_type = "" + for word in name.split(): + if not word: + continue + if word.endswith(']'): + word = word[:-1] + if word.isdigit(): + if lastword_type == "cn": + self.cn_name = "%s %s" % (self.cn_name or "", word) + elif lastword_type == "en": + self.en_name = "%s %s" % (self.en_name or "", word) + elif StringUtils.is_chinese(word): + self.cn_name = "%s %s" % (self.cn_name or "", word) + lastword_type = "cn" + else: + self.en_name = "%s %s" % (self.en_name or "", word) + lastword_type = "en" + if self.cn_name: + _, self.cn_name, _, _, _, _ = StringUtils.get_keyword_from_string(self.cn_name) + if self.cn_name: + self.cn_name = re.sub(r'%s' % self._name_nostring_re, '', self.cn_name, flags=re.IGNORECASE).strip() + self.cn_name = zhconv.convert(self.cn_name, "zh-hans") + if self.en_name: + self.en_name = re.sub(r'%s' % self._name_nostring_re, '', self.en_name, flags=re.IGNORECASE).strip().title() + self._name = StringUtils.str_title(self.en_name) + # 年份 + year = anitopy_info.get("anime_year") + if str(year).isdigit(): + self.year = str(year) + # 季号 + anime_season = anitopy_info.get("anime_season") + if isinstance(anime_season, list): + if len(anime_season) == 1: + begin_season = anime_season[0] + end_season = None + else: + begin_season = anime_season[0] + end_season = anime_season[-1] + elif anime_season: + begin_season = anime_season + end_season = None + else: + begin_season = None + end_season = None + if begin_season: + self.begin_season = int(begin_season) + if end_season and int(end_season) != self.begin_season: + self.end_season = int(end_season) + self.total_seasons = (self.end_season - self.begin_season) + 1 + else: + self.total_seasons = 1 + self.type = MediaType.TV + # 集号 + episode_number = anitopy_info.get("episode_number") + if isinstance(episode_number, list): + if len(episode_number) == 1: + begin_episode = episode_number[0] + end_episode = None + else: + begin_episode = episode_number[0] + end_episode = episode_number[-1] + elif episode_number: + begin_episode = episode_number + end_episode = None + else: + begin_episode = None + end_episode = None + if begin_episode: + try: + self.begin_episode = int(begin_episode) + if end_episode and int(end_episode) != self.begin_episode: + self.end_episode = int(end_episode) + self.total_episodes = (self.end_episode - self.begin_episode) + 1 + else: + self.total_episodes = 1 + except Exception as err: + ExceptionUtils.exception_traceback(err) + self.begin_episode = None + self.end_episode = None + self.type = MediaType.TV + # 类型 + if not self.type: + anime_type = anitopy_info.get('anime_type') + if isinstance(anime_type, list): + anime_type = anime_type[0] + if anime_type and anime_type.upper() == "TV": + self.type = MediaType.TV + else: + self.type = MediaType.MOVIE + # 分辨率 + self.resource_pix = anitopy_info.get("video_resolution") + if isinstance(self.resource_pix, list): + self.resource_pix = self.resource_pix[0] + if self.resource_pix: + if re.search(r'x', self.resource_pix, re.IGNORECASE): + self.resource_pix = re.split(r'[Xx]', self.resource_pix)[-1] + "p" + else: + self.resource_pix = self.resource_pix.lower() + if str(self.resource_pix).isdigit(): + self.resource_pix = str(self.resource_pix) + "p" + # 制作组/字幕组 + self.resource_team = \ + anitopy_info_origin.get("release_group") or \ + ReleaseGroupsMatcher().match(title=original_title) or None + # 视频编码 + self.video_encode = anitopy_info.get("video_term") + if isinstance(self.video_encode, list): + self.video_encode = self.video_encode[0] + # 音频编码 + self.audio_encode = anitopy_info.get("audio_term") + if isinstance(self.audio_encode, list): + self.audio_encode = self.audio_encode[0] + # 解析副标题,只要季和集 + self.init_subtitle(self.org_string) + if not self._subtitle_flag and self.subtitle: + self.init_subtitle(self.subtitle) + if not self.type: + self.type = MediaType.TV + except Exception as e: + ExceptionUtils.exception_traceback(e) + + @staticmethod + def __prepare_title(title): + """ + 对命名进行预处理 + """ + if not title: + return title + # 所有【】换成[] + title = title.replace("【", "[").replace("】", "]").strip() + # 截掉xx番剧漫 + match = re.search(r"新番|月?番|[日美国][漫剧]", title) + if match and match.span()[1] < len(title) - 1: + title = re.sub(".*番.|.*[日美国][漫剧].", "", title) + elif match: + title = title[:title.rfind('[')] + # 截掉分类 + first_item = title.split(']')[0] + if first_item and re.search(r"[动漫画纪录片电影视连续剧集日美韩中港台海外亚洲华语大陆综艺原盘高清]{2,}|TV|Animation|Movie|Documentar|Anime", + zhconv.convert(first_item, "zh-hans"), + re.IGNORECASE): + title = re.sub(r"^[^]]*]", "", title).strip() + # 去掉大小 + title = re.sub(r'[0-9.]+\s*[MGT]i?B(?![A-Z]+)', "", title, flags=re.IGNORECASE) + # 将TVxx改为xx + title = re.sub(r"\[TV\s+(\d{1,4})", r"[\1", title, flags=re.IGNORECASE) + # 将4K转为2160p + title = re.sub(r'\[4k]', '2160p', title, flags=re.IGNORECASE) + # 处理/分隔的中英文标题 + names = title.split("]") + if len(names) > 1 and title.find("- ") == -1: + titles = [] + for name in names: + if not name: + continue + left_char = '' + if name.startswith('['): + left_char = '[' + name = name[1:] + if name and name.find("/") != -1: + if name.split("/")[-1].strip(): + titles.append("%s%s" % (left_char, name.split("/")[-1].strip())) + else: + titles.append("%s%s" % (left_char, name.split("/")[0].strip())) + elif name: + if StringUtils.is_chinese(name) and not StringUtils.is_all_chinese(name): + if not re.search(r"\[\d+", name, re.IGNORECASE): + name = re.sub(r'[\d|#::\-()()\u4e00-\u9fff]', '', name).strip() + if not name or name.strip().isdigit(): + continue + if name == '[': + titles.append("") + else: + titles.append("%s%s" % (left_char, name.strip())) + return "]".join(titles) + return title diff --git a/app/media/meta/metainfo.py b/app/media/meta/metainfo.py new file mode 100644 index 0000000..ad98482 --- /dev/null +++ b/app/media/meta/metainfo.py @@ -0,0 +1,65 @@ +import os.path +import regex as re + +import log +from app.helper import WordsHelper +from app.media.meta.metaanime import MetaAnime +from app.media.meta.metavideo import MetaVideo +from app.utils.types import MediaType +from config import RMT_MEDIAEXT + + +def MetaInfo(title, subtitle=None, mtype=None): + """ + 媒体整理入口,根据名称和副标题,判断是哪种类型的识别,返回对应对象 + :param title: 标题、种子名、文件名 + :param subtitle: 副标题、描述 + :param mtype: 指定识别类型,为空则自动识别类型 + :return: MetaAnime、MetaVideo + """ + + # 应用自定义识别词 + title, msg, used_info = WordsHelper().process(title) + if subtitle: + subtitle, _, _ = WordsHelper().process(subtitle) + + if msg: + for msg_item in msg: + log.warn("【Meta】%s" % msg_item) + + # 判断是否处理文件 + if title and os.path.splitext(title)[-1] in RMT_MEDIAEXT: + fileflag = True + else: + fileflag = False + + if mtype == MediaType.ANIME or is_anime(title): + meta_info = MetaAnime(title, subtitle, fileflag) + else: + meta_info = MetaVideo(title, subtitle, fileflag) + + meta_info.ignored_words = used_info.get("ignored") + meta_info.replaced_words = used_info.get("replaced") + meta_info.offset_words = used_info.get("offset") + + return meta_info + + +def is_anime(name): + """ + 判断是否为动漫 + :param name: 名称 + :return: 是否动漫 + """ + if not name: + return False + if re.search(r'【[+0-9XVPI-]+】\s*【', name, re.IGNORECASE): + return True + if re.search(r'\s+-\s+[\dv]{1,4}\s+', name, re.IGNORECASE): + return True + if re.search(r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}", name, + re.IGNORECASE): + return False + if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE): + return True + return False diff --git a/app/media/meta/metavideo.py b/app/media/meta/metavideo.py new file mode 100644 index 0000000..af33eec --- /dev/null +++ b/app/media/meta/metavideo.py @@ -0,0 +1,547 @@ +import os +import re + +from config import RMT_MEDIAEXT +from app.media.meta._base import MetaBase +from app.utils import StringUtils +from app.utils.tokens import Tokens +from app.utils.types import MediaType +from app.media.meta.release_groups import ReleaseGroupsMatcher + + +class MetaVideo(MetaBase): + """ + 识别电影、电视剧 + """ + # 控制标位区 + _stop_name_flag = False + _stop_cnname_flag = False + _last_token = "" + _last_token_type = "" + _continue_flag = True + _unknown_name_str = "" + _source = "" + _effect = [] + # 正则式区 + _season_re = r"S(\d{2})|^S(\d{1,2})$|S(\d{1,2})E" + _episode_re = r"EP?(\d{2,4})|^EP?(\d{1,4})$|S\d{1,2}EP?(\d{1,4})$" + _part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)" + _roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$" + _source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$" + _effect_re = r"^REMUX$|^UHD$|^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$" + _resources_type_re = r"%s|%s" % (_source_re, _effect_re) + _name_no_begin_re = r"^\[.+?]" + _name_no_chinese_re = r".*版|.*字幕" + _name_se_words = ['共', '第', '季', '集', '话', '話', '期'] + _name_nostring_re = r"^PTS|^JADE|^AOD|^CHC|^[A-Z]{1,4}TV[\-0-9UVHDK]*" \ + r"|HBO$|\s+HBO|\d{1,2}th|\d{1,2}bit|NETFLIX|AMAZON|IMAX|^3D|\s+3D|^BBC\s+|\s+BBC|BBC$|DISNEY\+?|XXX|\s+DC$" \ + r"|[第\s共]+[0-9一二三四五六七八九十\-\s]+季" \ + r"|[第\s共]+[0-9一二三四五六七八九十\-\s]+[集话話]" \ + r"|连载|日剧|美剧|电视剧|动画片|动漫|欧美|西德|日韩|超高清|高清|蓝光|翡翠台|梦幻天堂·龙网|★?\d*月?新番" \ + r"|最终季|合集|[多中国英葡法俄日韩德意西印泰台港粤双文语简繁体特效内封官译外挂]+字幕|版本|出品|台版|港版|\w+字幕组" \ + r"|未删减版|UNCUT$|UNRATE$|WITH EXTRAS$|RERIP$|SUBBED$|PROPER$|REPACK$|SEASON$|EPISODE$|Complete$|Extended$|Extended Version$" \ + r"|S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}" \ + r"|CD[\s.]*[1-9]|DVD[\s.]*[1-9]|DISK[\s.]*[1-9]|DISC[\s.]*[1-9]" \ + r"|[248]K|\d{3,4}[PIX]+" \ + r"|CD[\s.]*[1-9]|DVD[\s.]*[1-9]|DISK[\s.]*[1-9]|DISC[\s.]*[1-9]" + _resources_pix_re = r"^[SBUHD]*(\d{3,4}[PI]+)|\d{3,4}X(\d{3,4})" + _resources_pix_re2 = r"(^[248]+K)" + _video_encode_re = r"^[HX]26[45]$|^AVC$|^HEVC$|^VC\d?$|^MPEG\d?$|^Xvid$|^DivX$|^HDR\d*$" + _audio_encode_re = r"^DTS\d?$|^DTSHD$|^DTSHDMA$|^Atmos$|^TrueHD\d?$|^AC3$|^\dAudios?$|^DDP\d?$|^DD\d?$|^LPCM\d?$|^AAC\d?$|^FLAC\d?$|^HD\d?$|^MA\d?$" + + def __init__(self, title, subtitle=None, fileflag=False): + super().__init__(title, subtitle, fileflag) + if not title: + return + original_title = title + self._source = "" + self._effect = [] + # 判断是否纯数字命名 + if os.path.splitext(title)[-1] in RMT_MEDIAEXT \ + and os.path.splitext(title)[0].isdigit() \ + and len(os.path.splitext(title)[0]) < 5: + self.begin_episode = int(os.path.splitext(title)[0]) + self.type = MediaType.TV + return + # 去掉名称中第1个[]的内容 + title = re.sub(r'%s' % self._name_no_begin_re, "", title, count=1) + # 把xxxx-xxxx年份换成前一个年份,常出现在季集上 + title = re.sub(r'([\s.]+)(\d{4})-(\d{4})', r'\1\2', title) + # 把大小去掉 + title = re.sub(r'[0-9.]+\s*[MGT]i?B(?![A-Z]+)', "", title, flags=re.IGNORECASE) + # 把年月日去掉 + title = re.sub(r'\d{4}[\s._-]\d{1,2}[\s._-]\d{1,2}', "", title) + # 拆分tokens + tokens = Tokens(title) + self.tokens = tokens + # 解析名称、年份、季、集、资源类型、分辨率等 + token = tokens.get_next() + while token: + # Part + self.__init_part(token) + # 标题 + if self._continue_flag: + self.__init_name(token) + # 年份 + if self._continue_flag: + self.__init_year(token) + # 分辨率 + if self._continue_flag: + self.__init_resource_pix(token) + # 季 + if self._continue_flag: + self.__init_season(token) + # 集 + if self._continue_flag: + self.__init_episode(token) + # 资源类型 + if self._continue_flag: + self.__init_resource_type(token) + # 视频编码 + if self._continue_flag: + self.__init_video_encode(token) + # 音频编码 + if self._continue_flag: + self.__init_audio_encode(token) + # 取下一个,直到没有为卡 + token = tokens.get_next() + self._continue_flag = True + # 合成质量 + if self._effect: + self._effect.reverse() + self.resource_effect = " ".join(self._effect) + if self._source: + self.resource_type = self._source.strip() + # 提取原盘DIY + if self.resource_type and "BluRay" in self.resource_type: + if (self.subtitle and re.findall(r'D[Ii]Y', self.subtitle)) \ + or re.findall(r'-D[Ii]Y@', original_title): + self.resource_type = f"{self.resource_type} DIY" + # 解析副标题,只要季和集 + self.init_subtitle(self.org_string) + if not self._subtitle_flag and self.subtitle: + self.init_subtitle(self.subtitle) + # 没有识别出类型时默认为电影 + if not self.type: + self.type = MediaType.MOVIE + # 去掉名字中不需要的干扰字符,过短的纯数字不要 + self.cn_name = self.__fix_name(self.cn_name) + self.en_name = StringUtils.str_title(self.__fix_name(self.en_name)) + # 处理part + if self.part and self.part.upper() == "PART": + self.part = None + # 制作组/字幕组 + self.resource_team = ReleaseGroupsMatcher().match(title=original_title) or None + + def __fix_name(self, name): + if not name: + return name + name = re.sub(r'%s' % self._name_nostring_re, '', name, + flags=re.IGNORECASE).strip() + name = re.sub(r'\s+', ' ', name) + if name.isdigit() \ + and int(name) < 1800 \ + and not self.year \ + and not self.begin_season \ + and not self.resource_pix \ + and not self.resource_type \ + and not self.audio_encode \ + and not self.video_encode: + if self.begin_episode is None: + self.begin_episode = int(name) + name = None + elif self.is_in_episode(int(name)) and not self.begin_season: + name = None + return name + + def __init_name(self, token): + if not token: + return + # 回收标题 + if self._unknown_name_str: + if not self.cn_name: + if not self.en_name: + self.en_name = self._unknown_name_str + elif self._unknown_name_str != self.year: + self.en_name = "%s %s" % (self.en_name, self._unknown_name_str) + self._last_token_type = "enname" + self._unknown_name_str = "" + if self._stop_name_flag: + return + if token.upper() == "AKA": + self._continue_flag = False + self._stop_name_flag = True + return + if token in self._name_se_words: + self._last_token_type = 'name_se_words' + return + if StringUtils.is_chinese(token): + # 含有中文,直接做为标题(连着的数字或者英文会保留),且不再取用后面出现的中文 + self._last_token_type = "cnname" + if not self.cn_name: + self.cn_name = token + elif not self._stop_cnname_flag: + if not re.search("%s" % self._name_no_chinese_re, token, flags=re.IGNORECASE) \ + and not re.search("%s" % self._name_se_words, token, flags=re.IGNORECASE): + self.cn_name = "%s %s" % (self.cn_name, token) + self._stop_cnname_flag = True + else: + is_roman_digit = re.search(self._roman_numerals, token) + # 阿拉伯数字或者罗马数字 + if token.isdigit() or is_roman_digit: + # 第季集后面的不要 + if self._last_token_type == 'name_se_words': + return + if self.get_name(): + # 名字后面以 0 开头的不要,极有可能是集 + if token.startswith('0'): + return + # 检查是否真正的数字 + if token.isdigit(): + try: + int(token) + except ValueError: + return + # 中文名后面跟的数字不是年份的极有可能是集 + if not is_roman_digit \ + and self._last_token_type == "cnname" \ + and int(token) < 1900: + return + if (token.isdigit() and len(token) < 4) or is_roman_digit: + # 4位以下的数字或者罗马数字,拼装到已有标题中 + if self._last_token_type == "cnname": + self.cn_name = "%s %s" % (self.cn_name, token) + elif self._last_token_type == "enname": + self.en_name = "%s %s" % (self.en_name, token) + self._continue_flag = False + elif token.isdigit() and len(token) == 4: + # 4位数字,可能是年份,也可能真的是标题的一部分,也有可能是集 + if not self._unknown_name_str: + self._unknown_name_str = token + else: + # 名字未出现前的第一个数字,记下来 + if not self._unknown_name_str: + self._unknown_name_str = token + elif re.search(r"%s" % self._season_re, token, re.IGNORECASE) \ + or re.search(r"%s" % self._episode_re, token, re.IGNORECASE) \ + or re.search(r"(%s)" % self._resources_type_re, token, re.IGNORECASE) \ + or re.search(r"%s" % self._resources_pix_re, token, re.IGNORECASE): + # 季集等不要 + self._stop_name_flag = True + return + else: + # 后缀名不要 + if ".%s".lower() % token in RMT_MEDIAEXT: + return + # 英文或者英文+数字,拼装起来 + if self.en_name: + self.en_name = "%s %s" % (self.en_name, token) + else: + self.en_name = token + self._last_token_type = "enname" + + def __init_part(self, token): + if not self.get_name(): + return + if not self.year \ + and not self.begin_season \ + and not self.begin_episode \ + and not self.resource_pix \ + and not self.resource_type: + return + re_res = re.search(r"%s" % self._part_re, token, re.IGNORECASE) + if re_res: + if not self.part: + self.part = re_res.group(1) + nextv = self.tokens.cur() + if nextv \ + and ((nextv.isdigit() and (len(nextv) == 1 or len(nextv) == 2 and nextv.startswith('0'))) + or nextv.upper() in ['A', 'B', 'C', 'I', 'II', 'III']): + self.part = "%s%s" % (self.part, nextv) + self.tokens.get_next() + self._last_token_type = "part" + self._continue_flag = False + self._stop_name_flag = False + + def __init_year(self, token): + if not self.get_name(): + return + if not token.isdigit(): + return + if len(token) != 4: + return + if not 1900 < int(token) < 2050: + return + if self.year: + if self.en_name: + self.en_name = "%s %s" % (self.en_name, self.year) + elif self.cn_name: + self.cn_name = "%s %s" % (self.cn_name, self.year) + self.year = token + self._last_token_type = "year" + self._continue_flag = False + self._stop_name_flag = True + + def __init_resource_pix(self, token): + if not self.get_name(): + return + re_res = re.findall(r"%s" % self._resources_pix_re, token, re.IGNORECASE) + if re_res: + self._last_token_type = "pix" + self._continue_flag = False + self._stop_name_flag = True + resource_pix = None + for pixs in re_res: + if isinstance(pixs, tuple): + pix_t = None + for pix_i in pixs: + if pix_i: + pix_t = pix_i + break + if pix_t: + resource_pix = pix_t + else: + resource_pix = pixs + if resource_pix and not self.resource_pix: + self.resource_pix = resource_pix.lower() + break + if self.resource_pix \ + and self.resource_pix.isdigit() \ + and self.resource_pix[-1] not in 'kpi': + self.resource_pix = "%sp" % self.resource_pix + else: + re_res = re.search(r"%s" % self._resources_pix_re2, token, re.IGNORECASE) + if re_res: + self._last_token_type = "pix" + self._continue_flag = False + self._stop_name_flag = True + if not self.resource_pix: + self.resource_pix = re_res.group(1).lower() + + def __init_season(self, token): + re_res = re.findall(r"%s" % self._season_re, token, re.IGNORECASE) + if re_res: + self._last_token_type = "season" + self.type = MediaType.TV + self._stop_name_flag = True + self._continue_flag = True + for se in re_res: + if isinstance(se, tuple): + se_t = None + for se_i in se: + if se_i and str(se_i).isdigit(): + se_t = se_i + break + if se_t: + se = int(se_t) + else: + break + else: + se = int(se) + if self.begin_season is None: + self.begin_season = se + self.total_seasons = 1 + else: + if se > self.begin_season: + self.end_season = se + self.total_seasons = (self.end_season - self.begin_season) + 1 + if self.fileflag and self.total_seasons > 1: + self.end_season = None + self.total_seasons = 1 + elif token.isdigit(): + try: + int(token) + except ValueError: + return + if self._last_token_type == "SEASON" \ + and self.begin_season is None \ + and len(token) < 3: + self.begin_season = int(token) + self.total_seasons = 1 + self._last_token_type = "season" + self._stop_name_flag = True + self._continue_flag = False + self.type = MediaType.TV + elif token.upper() == "SEASON" and self.begin_season is None: + self._last_token_type = "SEASON" + + def __init_episode(self, token): + re_res = re.findall(r"%s" % self._episode_re, token, re.IGNORECASE) + if re_res: + self._last_token_type = "episode" + self._continue_flag = False + self._stop_name_flag = True + self.type = MediaType.TV + for se in re_res: + if isinstance(se, tuple): + se_t = None + for se_i in se: + if se_i and str(se_i).isdigit(): + se_t = se_i + break + if se_t: + se = int(se_t) + else: + break + else: + se = int(se) + if self.begin_episode is None: + self.begin_episode = se + self.total_episodes = 1 + else: + if se > self.begin_episode: + self.end_episode = se + self.total_episodes = (self.end_episode - self.begin_episode) + 1 + if self.fileflag and self.total_episodes > 2: + self.end_episode = None + self.total_episodes = 1 + elif token.isdigit(): + try: + int(token) + except ValueError: + return + if self.begin_episode is not None \ + and self.end_episode is None \ + and len(token) < 5 \ + and int(token) > self.begin_episode \ + and self._last_token_type == "episode": + self.end_episode = int(token) + self.total_episodes = (self.end_episode - self.begin_episode) + 1 + if self.fileflag and self.total_episodes > 2: + self.end_episode = None + self.total_episodes = 1 + self._continue_flag = False + self.type = MediaType.TV + elif self.begin_episode is None \ + and 1 < len(token) < 4 \ + and self._last_token_type != "year" \ + and self._last_token_type != "videoencode" \ + and token != self._unknown_name_str: + self.begin_episode = int(token) + self.total_episodes = 1 + self._last_token_type = "episode" + self._continue_flag = False + self._stop_name_flag = True + self.type = MediaType.TV + elif self._last_token_type == "EPISODE" \ + and self.begin_episode is None \ + and len(token) < 5: + self.begin_episode = int(token) + self.total_episodes = 1 + self._last_token_type = "episode" + self._continue_flag = False + self._stop_name_flag = True + self.type = MediaType.TV + elif token.upper() == "EPISODE": + self._last_token_type = "EPISODE" + + def __init_resource_type(self, token): + if not self.get_name(): + return + source_res = re.search(r"(%s)" % self._source_re, token, re.IGNORECASE) + if source_res: + self._last_token_type = "source" + self._continue_flag = False + self._stop_name_flag = True + if not self._source: + self._source = source_res.group(1) + self._last_token = self._source.upper() + return + elif token.upper() == "DL" \ + and self._last_token_type == "source" \ + and self._last_token == "WEB": + self._source = "WEB-DL" + self._continue_flag = False + return + elif token.upper() == "RAY" \ + and self._last_token_type == "source" \ + and self._last_token == "BLU": + self._source = "BluRay" + self._continue_flag = False + return + elif token.upper() == "WEBDL": + self._source = "WEB-DL" + self._continue_flag = False + return + effect_res = re.search(r"(%s)" % self._effect_re, token, re.IGNORECASE) + if effect_res: + self._last_token_type = "effect" + self._continue_flag = False + self._stop_name_flag = True + effect = effect_res.group(1) + if effect not in self._effect: + self._effect.append(effect) + self._last_token = effect.upper() + + def __init_video_encode(self, token): + if not self.get_name(): + return + if not self.year \ + and not self.resource_pix \ + and not self.resource_type \ + and not self.begin_season \ + and not self.begin_episode: + return + re_res = re.search(r"(%s)" % self._video_encode_re, token, re.IGNORECASE) + if re_res: + self._continue_flag = False + self._stop_name_flag = True + self._last_token_type = "videoencode" + if not self.video_encode: + self.video_encode = re_res.group(1).upper() + self._last_token = self.video_encode + elif self.video_encode == "10bit": + self.video_encode = f"{re_res.group(1).upper()} 10bit" + self._last_token = re_res.group(1).upper() + elif token.upper() in ['H', 'X']: + self._continue_flag = False + self._stop_name_flag = True + self._last_token_type = "videoencode" + self._last_token = token.upper() if token.upper() == "H" else token.lower() + elif token in ["264", "265"] \ + and self._last_token_type == "videoencode" \ + and self._last_token in ['H', 'X']: + self.video_encode = "%s%s" % (self._last_token, token) + elif token.isdigit() \ + and self._last_token_type == "videoencode" \ + and self._last_token in ['VC', 'MPEG']: + self.video_encode = "%s%s" % (self._last_token, token) + elif token.upper() == "10BIT": + self._last_token_type = "videoencode" + if not self.video_encode: + self.video_encode = "10bit" + else: + self.video_encode = f"{self.video_encode} 10bit" + + def __init_audio_encode(self, token): + if not self.get_name(): + return + if not self.year \ + and not self.resource_pix \ + and not self.resource_type \ + and not self.begin_season \ + and not self.begin_episode: + return + re_res = re.search(r"(%s)" % self._audio_encode_re, token, re.IGNORECASE) + if re_res: + self._continue_flag = False + self._stop_name_flag = True + self._last_token_type = "audioencode" + self._last_token = re_res.group(1).upper() + if not self.audio_encode: + self.audio_encode = re_res.group(1) + else: + if self.audio_encode.upper() == "DTS": + self.audio_encode = "%s-%s" % (self.audio_encode, re_res.group(1)) + else: + self.audio_encode = "%s %s" % (self.audio_encode, re_res.group(1)) + elif token.isdigit() \ + and self._last_token_type == "audioencode": + if self.audio_encode: + if self._last_token.isdigit(): + self.audio_encode = "%s.%s" % (self.audio_encode, token) + elif self.audio_encode[-1].isdigit(): + self.audio_encode = "%s %s.%s" % (self.audio_encode[:-1], self.audio_encode[-1], token) + else: + self.audio_encode = "%s %s" % (self.audio_encode, token) + self._last_token = token diff --git a/app/media/meta/release_groups.py b/app/media/meta/release_groups.py new file mode 100644 index 0000000..71e7030 --- /dev/null +++ b/app/media/meta/release_groups.py @@ -0,0 +1,103 @@ +import re +from config import Config + + +class ReleaseGroupsMatcher(object): + """ + 识别制作组、字幕组 + """ + __config = None + __release_groups = None + RELEASE_GROUPS = { + "0ff": ['FF(?:(?:A|WE)B|CD|E(?:DU|B)|TV)'], + "1pt": [], + "52pt": [], + "audiences": ['Audies', 'AD(?:Audio|E(?:|book)|Music|Web)'], + "azusa": [], + "beitai": ['BeiTai'], + "btschool": ['Bts(?:CHOOL|HD|PAD|TV)', 'Zone'], + "carpt": ['CarPT'], + "chdbits": ['CHD(?:|Bits|PAD|(?:|HK)TV|WEB)', 'StBOX', 'OneHD', 'Lee', 'xiaopie'], + "discfan": [], + "dragonhd": [], + "eastgame": ['(?:(?:iNT|(?:HALFC|Mini(?:S|H|FH)D))-|)TLF'], + "filelist": [], + "gainbound": ['(?:DG|GBWE)B'], + "hares": ['Hares(?:|(?:M|T)V|Web)'], + "hd4fans": [], + "hdarea": ['HDA(?:pad|rea|TV)', 'EPiC'], + "hdatmos": [], + "hdbd": [], + "hdchina": ['HDC(?:|hina|TV)', 'k9611', 'tudou', 'iHD'], + "hddolby": ['D(?:ream|BTV)', '(?:HD|QHstudI)o'], + "hdfans": ['beAst(?:|TV)'], + "hdhome": ['HDH(?:|ome|Pad|TV|WEB)'], + "hdpt": ['HDPT(?:|Web)'], + "hdsky": ['HDS(?:|ky|TV|Pad|WEB)', 'AQLJ'], + "hdtime": [], + "HDU": [], + "hdvideo": [], + "hdzone": ['HDZ(?:|one)'], + "hhanclub": ['HHWEB'], + "hitpt": [], + "htpt": ['HTPT'], + "iptorrents": [], + "joyhd": [], + "keepfrds": ['FRDS', 'Yumi', 'cXcY'], + "lemonhd": ['L(?:eague(?:(?:C|H)D|(?:M|T)V|NF|WEB)|HD)', 'i18n', 'CiNT'], + "mteam": ['MTeam(?:|TV)', 'MPAD'], + "nanyangpt": [], + "nicept": [], + "oshen": [], + "ourbits": ['Our(?:Bits|TV)', 'FLTTH', 'Ao', 'PbK', 'MGs', 'iLove(?:HD|TV)'], + "piggo": ['PiGo(?:NF|(?:H|WE)B)'], + "ptchina": [], + "pterclub": ['PTer(?:|DIY|Game|(?:M|T)V|WEB)'], + "pthome": ['PTH(?:|Audio|eBook|music|ome|tv|WEB)'], + "ptmsg": [], + "ptsbao": ['PTsbao', 'OPS', 'F(?:Fans(?:AIeNcE|BD|D(?:VD|IY)|TV|WEB)|HDMv)', 'SGXT'], + "pttime": [], + "putao": ['PuTao'], + "soulvoice": [], + "springsunday": ['CMCT(?:|V)'], + "sharkpt": ['Shark(?:|WEB|DIY|TV|MV)'], + "tccf": [], + "tjupt": ['TJUPT'], + "totheglory": ['TTG', 'WiKi', 'NGB', 'DoA', '(?:ARi|ExRE)N'], + "U2": [], + "ultrahd": [], + "others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:|yG)', + 'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )'], + "anime": ['ANi', 'HYSUB', 'KTXP', 'LoliHouse', 'MCE', 'Nekomoe kissaten', '(?:Lilith|NC)-Raws', '织梦字幕组'] + } + + def __init__(self): + self.__config = Config() + release_groups = [] + for site_groups in self.RELEASE_GROUPS.values(): + for release_group in site_groups: + release_groups.append(release_group) + custom_release_groups = (self.__config.get_config('laboratory') or {}).get('release_groups') + if custom_release_groups: + if custom_release_groups.startswith(';'): + custom_release_groups = custom_release_groups[1:] + if custom_release_groups.endswith(';'): + custom_release_groups = custom_release_groups[:-1] + custom_release_groups = custom_release_groups.replace(";", "|") + self.__release_groups = f"{'|'.join(release_groups)}|{custom_release_groups}" + else: + self.__release_groups = '|'.join(release_groups) + + def match(self, title=None, groups=None): + """ + :param title: 资源标题或文件名 + :param groups: 制作组/字幕组 + :return: 匹配结果 + """ + if not title: + return "" + if not groups: + groups = self.__release_groups + title = f"{title} " + groups_re = re.compile(r"(?<=[-@\[£【])(?:%s)(?=[@.\s\]\[】])" % groups, re.I) + return '@'.join(re.findall(groups_re, title)) diff --git a/app/media/scraper.py b/app/media/scraper.py new file mode 100644 index 0000000..ebb5faa --- /dev/null +++ b/app/media/scraper.py @@ -0,0 +1,542 @@ +import os.path +import time +from xml.dom import minidom + +import log +from app.helper import FfmpegHelper +from app.media.douban import DouBan +from config import TMDB_IMAGE_W500_URL +from app.utils import DomUtils, RequestUtils, ExceptionUtils +from app.utils.types import MediaType +from app.media import Media + + +class Scraper: + media = None + + def __init__(self): + self.media = Media() + self.douban = DouBan() + + def __gen_common_nfo(self, + tmdbinfo: dict, + doubaninfo: dict, + scraper_nfo: dict, + doc, + root, + chinese=False): + if scraper_nfo.get("basic"): + # 添加时间 + DomUtils.add_node(doc, root, "dateadded", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))) + # TMDB + DomUtils.add_node(doc, root, "tmdbid", tmdbinfo.get("id") or "") + uniqueid_tmdb = DomUtils.add_node(doc, root, "uniqueid", tmdbinfo.get("id") or "") + uniqueid_tmdb.setAttribute("type", "tmdb") + uniqueid_tmdb.setAttribute("default", "true") + # TVDB IMDB + if tmdbinfo.get("external_ids"): + tvdbid = tmdbinfo.get("external_ids", {}).get("tvdb_id", 0) + if tvdbid: + DomUtils.add_node(doc, root, "tvdbid", tvdbid) + uniqueid_tvdb = DomUtils.add_node(doc, root, "uniqueid", tvdbid) + uniqueid_tvdb.setAttribute("type", "tvdb") + imdbid = tmdbinfo.get("external_ids", {}).get("imdb_id", "") + if imdbid: + DomUtils.add_node(doc, root, "imdbid", imdbid) + uniqueid_imdb = DomUtils.add_node(doc, root, "uniqueid", imdbid) + uniqueid_imdb.setAttribute("type", "imdb") + uniqueid_imdb.setAttribute("default", "true") + uniqueid_tmdb.setAttribute("default", "false") + + # 简介 + xplot = DomUtils.add_node(doc, root, "plot") + xplot.appendChild(doc.createCDATASection(tmdbinfo.get("overview") or "")) + xoutline = DomUtils.add_node(doc, root, "outline") + xoutline.appendChild(doc.createCDATASection(tmdbinfo.get("overview") or "")) + if scraper_nfo.get("credits"): + # 导演 + directors, actors = self.media.get_tmdb_directors_actors(tmdbinfo=tmdbinfo) + if chinese: + directors, actors = self.__gen_people_chinese_info(directors, actors, doubaninfo) + for director in directors: + xdirector = DomUtils.add_node(doc, root, "director", director.get("name") or "") + xdirector.setAttribute("tmdbid", str(director.get("id") or "")) + # 演员 + for actor in actors: + xactor = DomUtils.add_node(doc, root, "actor") + DomUtils.add_node(doc, xactor, "name", actor.get("name") or "") + DomUtils.add_node(doc, xactor, "type", "Actor") + DomUtils.add_node(doc, xactor, "role", actor.get("role") or "") + DomUtils.add_node(doc, xactor, "order", actor.get("order") if actor.get("order") is not None else "") + DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "") + DomUtils.add_node(doc, xactor, "thumb", actor.get('image')) + DomUtils.add_node(doc, xactor, "profile", actor.get('profile')) + if scraper_nfo.get("basic"): + # 风格 + genres = tmdbinfo.get("genres") or [] + for genre in genres: + DomUtils.add_node(doc, root, "genre", genre.get("name") or "") + # 评分 + DomUtils.add_node(doc, root, "rating", tmdbinfo.get("vote_average") or "0") + return doc + + def gen_movie_nfo_file(self, + tmdbinfo: dict, + doubaninfo: dict, + scraper_movie_nfo: dict, + out_path, + file_name): + """ + 生成电影的NFO描述文件 + :param tmdbinfo: TMDB元数据 + :param doubaninfo: 豆瓣元数据 + :param scraper_movie_nfo: 刮削配置 + :param out_path: 电影根目录 + :param file_name: 电影文件名,不含后缀 + """ + # 开始生成XML + log.info("【Scraper】正在生成电影NFO文件:%s" % file_name) + doc = minidom.Document() + root = DomUtils.add_node(doc, doc, "movie") + # 公共部分 + doc = self.__gen_common_nfo(tmdbinfo=tmdbinfo, + doubaninfo=doubaninfo, + scraper_nfo=scraper_movie_nfo, + doc=doc, + root=root, + chinese=scraper_movie_nfo.get("credits_chinese")) + # 基础部分 + if scraper_movie_nfo.get("basic"): + # 标题 + DomUtils.add_node(doc, root, "title", tmdbinfo.get("title") or "") + DomUtils.add_node(doc, root, "originaltitle", tmdbinfo.get("original_title") or "") + # 发布日期 + DomUtils.add_node(doc, root, "premiered", tmdbinfo.get("release_date") or "") + # 年份 + DomUtils.add_node(doc, root, "year", + tmdbinfo.get("release_date")[:4] if tmdbinfo.get("release_date") else "") + # 保存 + self.__save_nfo(doc, os.path.join(out_path, "%s.nfo" % file_name)) + + def gen_tv_nfo_file(self, + tmdbinfo: dict, + doubaninfo: dict, + scraper_tv_nfo: dict, + out_path): + """ + 生成电视剧的NFO描述文件 + :param tmdbinfo: TMDB元数据 + :param doubaninfo: 豆瓣元数据 + :param scraper_tv_nfo: 刮削配置 + :param out_path: 电视剧根目录 + """ + # 开始生成XML + log.info("【Scraper】正在生成电视剧NFO文件:%s" % out_path) + doc = minidom.Document() + root = DomUtils.add_node(doc, doc, "tvshow") + # 公共部分 + doc = self.__gen_common_nfo(tmdbinfo=tmdbinfo, + doubaninfo=doubaninfo, + scraper_nfo=scraper_tv_nfo, + doc=doc, + root=root, + chinese=scraper_tv_nfo.get("credits_chinese")) + if scraper_tv_nfo.get("basic"): + # 标题 + DomUtils.add_node(doc, root, "title", tmdbinfo.get("name") or "") + DomUtils.add_node(doc, root, "originaltitle", tmdbinfo.get("original_name") or "") + # 发布日期 + DomUtils.add_node(doc, root, "premiered", tmdbinfo.get("first_air_date") or "") + # 年份 + DomUtils.add_node(doc, root, "year", + tmdbinfo.get("first_air_date")[:4] if tmdbinfo.get("first_air_date") else "") + DomUtils.add_node(doc, root, "season", "-1") + DomUtils.add_node(doc, root, "episode", "-1") + # 保存 + self.__save_nfo(doc, os.path.join(out_path, "tvshow.nfo")) + + def gen_tv_season_nfo_file(self, tmdbinfo: dict, season, out_path): + """ + 生成电视剧季的NFO描述文件 + :param tmdbinfo: TMDB季媒体信息 + :param season: 季号 + :param out_path: 电视剧季的目录 + """ + log.info("【Scraper】正在生成季NFO文件:%s" % out_path) + doc = minidom.Document() + root = DomUtils.add_node(doc, doc, "season") + # 添加时间 + DomUtils.add_node(doc, root, "dateadded", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))) + # 简介 + xplot = DomUtils.add_node(doc, root, "plot") + xplot.appendChild(doc.createCDATASection(tmdbinfo.get("overview") or "")) + xoutline = DomUtils.add_node(doc, root, "outline") + xoutline.appendChild(doc.createCDATASection(tmdbinfo.get("overview") or "")) + # 标题 + DomUtils.add_node(doc, root, "title", "季 %s" % season) + # 发行日期 + DomUtils.add_node(doc, root, "premiered", tmdbinfo.get("air_date") or "") + DomUtils.add_node(doc, root, "releasedate", tmdbinfo.get("air_date") or "") + # 发行年份 + DomUtils.add_node(doc, root, "year", tmdbinfo.get("air_date")[:4] if tmdbinfo.get("air_date") else "") + # seasonnumber + DomUtils.add_node(doc, root, "seasonnumber", season) + # 保存 + self.__save_nfo(doc, os.path.join(out_path, "season.nfo")) + + def gen_tv_episode_nfo_file(self, + tmdbinfo: dict, + scraper_tv_nfo, + season: int, + episode: int, + out_path, + file_name): + """ + 生成电视剧集的NFO描述文件 + :param tmdbinfo: TMDB元数据 + :param scraper_tv_nfo: 刮削配置 + :param season: 季号 + :param episode: 集号 + :param out_path: 电视剧季的目录 + :param file_name: 电视剧文件名,不含后缀 + """ + # 开始生成集的信息 + log.info("【Scraper】正在生成剧集NFO文件:%s" % file_name) + # 集的信息 + episode_detail = {} + for episode_info in tmdbinfo.get("episodes") or []: + if int(episode_info.get("episode_number")) == int(episode): + episode_detail = episode_info + if not episode_detail: + return + doc = minidom.Document() + root = DomUtils.add_node(doc, doc, "episodedetails") + if scraper_tv_nfo.get("episode_basic"): + # 添加时间 + DomUtils.add_node(doc, root, "dateadded", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))) + # TMDBID + uniqueid = DomUtils.add_node(doc, root, "uniqueid", tmdbinfo.get("id") or "") + uniqueid.setAttribute("type", "tmdb") + uniqueid.setAttribute("default", "true") + # tmdbid + DomUtils.add_node(doc, root, "tmdbid", tmdbinfo.get("id") or "") + # 标题 + DomUtils.add_node(doc, root, "title", episode_detail.get("name") or "第 %s 集" % episode) + # 简介 + xplot = DomUtils.add_node(doc, root, "plot") + xplot.appendChild(doc.createCDATASection(episode_detail.get("overview") or "")) + xoutline = DomUtils.add_node(doc, root, "outline") + xoutline.appendChild(doc.createCDATASection(episode_detail.get("overview") or "")) + # 发布日期 + DomUtils.add_node(doc, root, "aired", episode_detail.get("air_date") or "") + # 年份 + DomUtils.add_node(doc, root, "year", + episode_detail.get("air_date")[:4] if episode_detail.get("air_date") else "") + # 季 + DomUtils.add_node(doc, root, "season", season) + # 集 + DomUtils.add_node(doc, root, "episode", episode) + # 评分 + DomUtils.add_node(doc, root, "rating", episode_detail.get("vote_average") or "0") + if scraper_tv_nfo.get("episode_credits"): + # 导演 + directors = episode_detail.get("crew") or [] + for director in directors: + if director.get("known_for_department") == "Directing": + xdirector = DomUtils.add_node(doc, root, "director", director.get("name") or "") + xdirector.setAttribute("tmdbid", str(director.get("id") or "")) + # 演员 + actors = episode_detail.get("guest_stars") or [] + for actor in actors: + if actor.get("known_for_department") == "Acting": + xactor = DomUtils.add_node(doc, root, "actor") + DomUtils.add_node(doc, xactor, "name", actor.get("name") or "") + DomUtils.add_node(doc, xactor, "type", "Actor") + DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "") + # 保存文件 + self.__save_nfo(doc, os.path.join(out_path, os.path.join(out_path, "%s.nfo" % file_name))) + + @staticmethod + def __save_image(url, out_path, itype=''): + """ + 下载poster.jpg并保存 + """ + if not url or not out_path: + return + if itype: + image_path = os.path.join(out_path, "%s.%s" % (itype, str(url).split('.')[-1])) + else: + image_path = out_path + if os.path.exists(image_path): + return + try: + log.info(f"【Scraper】正在下载{itype}图片:{url} ...") + r = RequestUtils().get_res(url) + if r: + with open(file=image_path, + mode="wb") as img: + img.write(r.content) + log.info(f"【Scraper】{itype}图片已保存:{out_path}") + else: + log.info(f"【Scraper】{itype}图片下载失败,请检查网络连通性") + except Exception as err: + ExceptionUtils.exception_traceback(err) + + @staticmethod + def __save_nfo(doc, out_file): + xml_str = doc.toprettyxml(indent=" ", encoding="utf-8") + with open(out_file, "wb") as xml_file: + xml_file.write(xml_str) + + def gen_scraper_files(self, media, scraper_nfo, scraper_pic, dir_path, file_name, file_ext): + """ + 刮削元数据 + :param media: 已识别的媒体信息 + :param scraper_nfo: NFO刮削配置 + :param scraper_pic: 图片刮削配置 + :param dir_path: 文件路径 + :param file_name: 文件名,不含后缀 + :param file_ext: 文件后缀 + """ + if not scraper_nfo: + scraper_nfo = {} + if not scraper_pic: + scraper_pic = {} + try: + # 电影 + if media.type == MediaType.MOVIE: + scraper_movie_nfo = scraper_nfo.get("movie") + scraper_movie_pic = scraper_pic.get("movie") + # movie nfo + if scraper_movie_nfo.get("basic") or scraper_movie_nfo.get("credits"): + # 已存在时不处理 + if not os.path.exists(os.path.join(dir_path, "movie.nfo")) \ + and not os.path.exists(os.path.join(dir_path, "%s.nfo" % file_name)): + # 查询Douban信息 + if scraper_movie_nfo.get("credits") and scraper_movie_nfo.get("credits_chinese"): + doubaninfo = self.douban.get_douban_info(media) + else: + doubaninfo = None + # 生成电影描述文件 + self.gen_movie_nfo_file(tmdbinfo=media.tmdb_info, + doubaninfo=doubaninfo, + scraper_movie_nfo=scraper_movie_nfo, + out_path=dir_path, + file_name=file_name) + # poster + if scraper_movie_pic.get("poster"): + poster_image = media.get_poster_image(original=True) + if poster_image: + self.__save_image(poster_image, dir_path, "poster") + # backdrop + if scraper_movie_pic.get("backdrop"): + backdrop_image = media.get_backdrop_image(default=False, original=True) + if backdrop_image: + self.__save_image(backdrop_image, dir_path, "fanart") + # background + if scraper_movie_pic.get("background"): + background_image = media.fanart.get_background(media_type=media.type, queryid=media.tmdb_id) + if background_image: + self.__save_image(background_image, dir_path, "background") + # logo + if scraper_movie_pic.get("logo"): + logo_image = media.fanart.get_logo(media_type=media.type, queryid=media.tmdb_id) + if logo_image: + self.__save_image(logo_image, dir_path, "logo") + # disc + if scraper_movie_pic.get("disc"): + disc_image = media.fanart.get_disc(media_type=media.type, queryid=media.tmdb_id) + if disc_image: + self.__save_image(disc_image, dir_path, "disc") + # banner + if scraper_movie_pic.get("banner"): + banner_image = media.fanart.get_banner(media_type=media.type, queryid=media.tmdb_id) + if banner_image: + self.__save_image(banner_image, dir_path, "banner") + # thumb + if scraper_movie_pic.get("thumb"): + thumb_image = media.fanart.get_thumb(media_type=media.type, queryid=media.tmdb_id) + if thumb_image: + self.__save_image(thumb_image, dir_path, "thumb") + # 电视剧 + else: + scraper_tv_nfo = scraper_nfo.get("tv") + scraper_tv_pic = scraper_pic.get("tv") + # tv nfo + if not os.path.exists(os.path.join(os.path.dirname(dir_path), "tvshow.nfo")): + if scraper_tv_nfo.get("basic") or scraper_tv_nfo.get("credits"): + # 查询Douban信息 + if scraper_tv_nfo.get("credits") and scraper_tv_nfo.get("credits_chinese"): + doubaninfo = self.douban.get_douban_info(media) + else: + doubaninfo = None + # 根目录描述文件 + self.gen_tv_nfo_file(media.tmdb_info, doubaninfo, scraper_tv_nfo, os.path.dirname(dir_path)) + # poster + if scraper_tv_pic.get("poster"): + poster_image = media.get_poster_image(original=True) + if poster_image: + self.__save_image(poster_image, os.path.dirname(dir_path), "poster") + # backdrop + if scraper_tv_pic.get("backdrop"): + backdrop_image = media.get_backdrop_image(default=False, original=True) + if backdrop_image: + self.__save_image(backdrop_image, os.path.dirname(dir_path), "fanart") + # background + if scraper_tv_pic.get("background"): + background_image = media.fanart.get_background(media_type=media.type, queryid=media.tvdb_id) + if background_image: + self.__save_image(background_image, dir_path, "show") + # logo + if scraper_tv_pic.get("logo"): + logo_image = media.fanart.get_logo(media_type=media.type, queryid=media.tvdb_id) + if logo_image: + self.__save_image(logo_image, dir_path, "logo") + # clearart + if scraper_tv_pic.get("clearart"): + clearart_image = media.fanart.get_disc(media_type=media.type, queryid=media.tvdb_id) + if clearart_image: + self.__save_image(clearart_image, dir_path, "clearart") + # banner + if scraper_tv_pic.get("banner"): + banner_image = media.fanart.get_banner(media_type=media.type, queryid=media.tvdb_id) + if banner_image: + self.__save_image(banner_image, dir_path, "banner") + # thumb + if scraper_tv_pic.get("thumb"): + thumb_image = media.fanart.get_thumb(media_type=media.type, queryid=media.tvdb_id) + if thumb_image: + self.__save_image(thumb_image, dir_path, "thumb") + # season nfo + if scraper_tv_nfo.get("season_basic"): + if not os.path.exists(os.path.join(dir_path, "season.nfo")): + # season nfo + seasoninfo = self.media.get_tmdb_tv_season_detail(tmdbid=media.tmdb_id, + season=int(media.get_season_seq())) + if seasoninfo: + self.gen_tv_season_nfo_file(seasoninfo, int(media.get_season_seq()), dir_path) + # episode nfo + if scraper_tv_nfo.get("episode_basic") \ + or scraper_tv_nfo.get("episode_credits"): + if not os.path.exists(os.path.join(dir_path, "%s.nfo" % file_name)): + seasoninfo = self.media.get_tmdb_tv_season_detail(tmdbid=media.tmdb_id, + season=int(media.get_season_seq())) + if seasoninfo: + self.gen_tv_episode_nfo_file(tmdbinfo=seasoninfo, + scraper_tv_nfo=scraper_tv_nfo, + season=int(media.get_season_seq()), + episode=int(media.get_episode_seq()), + out_path=dir_path, + file_name=file_name) + # season poster + if scraper_tv_pic.get("season_poster"): + season_poster = "season%s-poster" % media.get_season_seq().rjust(2, '0') + seasonposter = media.fanart.get_seasonposter(media_type=media.type, + queryid=media.tvdb_id, + season=media.get_season_seq()) + if seasonposter: + self.__save_image(seasonposter, + os.path.dirname(dir_path), + season_poster) + else: + seasoninfo = self.media.get_tmdb_tv_season_detail(tmdbid=media.tmdb_id, + season=int(media.get_season_seq())) + if seasoninfo: + self.__save_image(TMDB_IMAGE_W500_URL % seasoninfo.get("poster_path"), + os.path.dirname(dir_path), + season_poster) + # season banner + if scraper_tv_pic.get("season_banner"): + seasonbanner = media.fanart.get_seasonbanner(media_type=media.type, + queryid=media.tvdb_id, + season=media.get_season_seq()) + if seasonbanner: + self.__save_image(seasonbanner, + os.path.dirname(dir_path), + "season%s-banner" % media.get_season_seq().rjust(2, '0')) + # season thumb + if scraper_tv_pic.get("season_thumb"): + seasonthumb = media.fanart.get_seasonthumb(media_type=media.type, + queryid=media.tvdb_id, + season=media.get_season_seq()) + if seasonthumb: + self.__save_image(seasonthumb, + os.path.dirname(dir_path), + "season%s-landscape" % media.get_season_seq().rjust(2, '0')) + # episode thumb + if scraper_tv_pic.get("episode_thumb"): + episode_thumb = os.path.join(dir_path, file_name + "-thumb.jpg") + if not os.path.exists(episode_thumb): + # 优先从TMDB查询 + episode_image = self.media.get_episode_images(tv_id=media.tmdb_id, + season_id=media.get_season_seq(), + episode_id=media.get_episode_seq(), + orginal=True) + if episode_image: + self.__save_image(episode_image, episode_thumb) + else: + # 从视频文件生成缩略图 + video_path = os.path.join(dir_path, file_name + file_ext) + log.info(f"【Scraper】正在生成缩略图:{video_path} ...") + FfmpegHelper().get_thumb_image_from_video(video_path=video_path, + image_path=episode_thumb) + log.info(f"【Scraper】缩略图生成完成:{episode_thumb}") + + except Exception as e: + ExceptionUtils.exception_traceback(e) + + def __gen_people_chinese_info(self, directors, actors, doubaninfo): + """ + 匹配豆瓣演职人员中文名 + """ + if doubaninfo: + directors_douban = doubaninfo.get("directors") or [] + actors_douban = doubaninfo.get("actors") or [] + # douban英文名姓和名分开匹配,(豆瓣中名前姓后,TMDB中不确定) + for director_douban in directors_douban: + if director_douban["latin_name"]: + director_douban["latin_name"] = director_douban.get("latin_name", "").lower().split(" ") + else: + director_douban["latin_name"] = director_douban.get("name", "").lower().split(" ") + for actor_douban in actors_douban: + if actor_douban["latin_name"]: + actor_douban["latin_name"] = actor_douban.get("latin_name", "").lower().split(" ") + else: + actor_douban["latin_name"] = actor_douban.get("name", "").lower().split(" ") + # 导演 + if directors: + for director in directors: + director_douban = self.__match_people_in_douban(director, directors_douban) + if director_douban: + director["name"] = director_douban.get("name") + else: + log.info("【Scraper】豆瓣该影片或剧集无导演 %s 信息" % director.get("name")) + # 演员 + if actors: + for actor in actors: + actor_douban = self.__match_people_in_douban(actor, actors_douban) + if actor_douban: + actor["name"] = actor_douban.get("name") + if actor_douban.get("character") != "演员": + actor["character"] = actor_douban.get("character")[2:] + else: + log.info("【Scraper】豆瓣该影片或剧集无演员 %s 信息" % actor.get("name")) + else: + log.info("【Scraper】豆瓣无该影片或剧集信息") + return directors, actors + + def __match_people_in_douban(self, people, peoples_douban): + """ + 名字加又名构成匹配列表 + """ + people_aka_names = self.media.get_tmdbperson_aka_names(people.get("id")) or [] + people_aka_names.append(people.get("name")) + for people_aka_name in people_aka_names: + for people_douban in peoples_douban: + latin_match_res = True + # 姓和名分开匹配 + for latin_name in people_douban.get("latin_name"): + latin_match_res = latin_match_res and (latin_name in people_aka_name.lower()) + if latin_match_res or (people_douban.get("name") == people_aka_name): + return people_douban + return None diff --git a/app/media/tmdbv3api/__init__.py b/app/media/tmdbv3api/__init__.py new file mode 100644 index 0000000..584a4d3 --- /dev/null +++ b/app/media/tmdbv3api/__init__.py @@ -0,0 +1,11 @@ +from .tmdb import TMDb +from .exceptions import TMDbException +from .objs.movie import Movie +from .objs.search import Search +from .objs.tv import TV +from .objs.person import Person +from .objs.find import Find +from .objs.discover import Discover +from .objs.trending import Trending +from .objs.episode import Episode +from .objs.genre import Genre diff --git a/app/media/tmdbv3api/as_obj.py b/app/media/tmdbv3api/as_obj.py new file mode 100644 index 0000000..24d73e6 --- /dev/null +++ b/app/media/tmdbv3api/as_obj.py @@ -0,0 +1,84 @@ +# encoding: utf-8 +import sys + +from app.media.tmdbv3api.exceptions import TMDbException + + +class AsObj: + def __init__(self, **entries): + if "success" in entries and entries["success"] is False: + raise TMDbException(entries["status_message"]) + for key, value in entries.items(): + if isinstance(value, list): + value = [AsObj(**item) if isinstance(item, dict) else item for item in value] + if isinstance(value, dict): + value = AsObj(**value) + setattr(self, key, value) + + def __delitem__(self, key): + return delattr(self, key) + + def __getitem__(self, key): + return getattr(self, key) + + def __iter__(self): + return iter(self.__dict__) + + def __len__(self): + return len(self.__dict__) + + def __repr__(self): + return str(self.__dict__) + + def __setitem__(self, key, value): + return setattr(self, key, value) + + def __str__(self): + return str(self.__dict__) + + if sys.version_info >= (3, 8): + def __reversed__(self): + return reversed(self.__dict__) + + if sys.version_info >= (3, 9): + def __class_getitem__(cls, key): + return cls.__dict__.__class_getitem__(key) + + def __ior__(self, value): + return self.__dict__.__ior__(value) + + def __or__(self, value): + return self.__dict__.__or__(value) + + def clear(self): + return self.__dict__.clear() + + def copy(self): + return AsObj(**self.__dict__.copy()) + + def fromkeys(self, keys, value=None): + return AsObj(**self.__dict__.fromkeys(keys, value)) + + def get(self, key, value=None): + return self.__dict__.get(key, value) + + def items(self): + return self.__dict__.items() + + def keys(self): + return self.__dict__.keys() + + def pop(self, key, value=None): + return self.__dict__.pop(key, value) + + def popitem(self): + return self.__dict__.popitem() + + def setdefault(self, key, value=None): + return self.__dict__.setdefault(key, value) + + def update(self, entries): + return self.__dict__.update(entries) + + def values(self): + return self.__dict__.values() diff --git a/app/media/tmdbv3api/exceptions.py b/app/media/tmdbv3api/exceptions.py new file mode 100644 index 0000000..e17eeb2 --- /dev/null +++ b/app/media/tmdbv3api/exceptions.py @@ -0,0 +1,2 @@ +class TMDbException(Exception): + pass diff --git a/app/media/tmdbv3api/objs/__init__.py b/app/media/tmdbv3api/objs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/media/tmdbv3api/objs/discover.py b/app/media/tmdbv3api/objs/discover.py new file mode 100644 index 0000000..b42b648 --- /dev/null +++ b/app/media/tmdbv3api/objs/discover.py @@ -0,0 +1,52 @@ +from app.media.tmdbv3api.tmdb import TMDb + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + + +class Discover(TMDb): + _urls = { + "movies": "/discover/movie", + "tvs": "/discover/tv" + } + + def discover_movies(self, params, page=1): + """ + Discover movies by different types of data like average rating, number of votes, genres and certifications. + :param params: dict + :param page: int + :return: + """ + if not params: + params = {} + if page: + params.update({"page": page}) + return self._get_obj( + self._call( + self._urls["movies"], + urlencode(params) + ), + "results" + ) + + def discover_tv_shows(self, params, page=1): + """ + Discover TV shows by different types of data like average rating, number of votes, genres, + the network they aired on and air dates. + :param params: dict + :param page: int + :return: + """ + if not params: + params = {} + if page: + params.update({"page": page}) + return self._get_obj( + self._call( + self._urls["tvs"], + urlencode(params) + ), + "results" + ) diff --git a/app/media/tmdbv3api/objs/episode.py b/app/media/tmdbv3api/objs/episode.py new file mode 100644 index 0000000..05729e1 --- /dev/null +++ b/app/media/tmdbv3api/objs/episode.py @@ -0,0 +1,24 @@ +from app.media.tmdbv3api.tmdb import TMDb + + +class Episode(TMDb): + _urls = { + "images": "/tv/%s/season/%s/episode/%s/images" + } + + def images(self, tv_id, season_num, episode_num, include_image_language=None): + """ + Get the images that belong to a TV episode. + :param tv_id: int + :param season_num: int + :param episode_num: int + :param include_image_language: str + :return: + """ + return self._get_obj( + self._call( + self._urls["images"] % (tv_id, season_num, episode_num), + "include_image_language=%s" % include_image_language if include_image_language else "", + ), + "stills" + ) diff --git a/app/media/tmdbv3api/objs/find.py b/app/media/tmdbv3api/objs/find.py new file mode 100644 index 0000000..d817f53 --- /dev/null +++ b/app/media/tmdbv3api/objs/find.py @@ -0,0 +1,12 @@ +from app.media.tmdbv3api.tmdb import TMDb + + +class Find(TMDb): + _urls = { + "find": "/find/%s" + } + + def find_by_imdbid(self, imdbid): + return self._call( + self._urls["find"] % imdbid, + "external_source=imdb_id") diff --git a/app/media/tmdbv3api/objs/genre.py b/app/media/tmdbv3api/objs/genre.py new file mode 100644 index 0000000..3d54633 --- /dev/null +++ b/app/media/tmdbv3api/objs/genre.py @@ -0,0 +1,22 @@ +from app.media.tmdbv3api.tmdb import TMDb + + +class Genre(TMDb): + _urls = { + "movie_list": "/genre/movie/list", + "tv_list": "/genre/tv/list" + } + + def movie_list(self): + """ + Get the list of official genres for movies. + :return: + """ + return self._get_obj(self._call(self._urls["movie_list"], ""), "genres") + + def tv_list(self): + """ + Get the list of official genres for TV shows. + :return: + """ + return self._get_obj(self._call(self._urls["tv_list"], ""), "genres") diff --git a/app/media/tmdbv3api/objs/movie.py b/app/media/tmdbv3api/objs/movie.py new file mode 100644 index 0000000..42a47fd --- /dev/null +++ b/app/media/tmdbv3api/objs/movie.py @@ -0,0 +1,295 @@ +import warnings + +from app.media.tmdbv3api.as_obj import AsObj +from app.media.tmdbv3api.tmdb import TMDb + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + + +class Movie(TMDb): + _urls = { + "details": "/movie/%s", + "alternative_titles": "/movie/%s/alternative_titles", + "changes": "/movie/%s/changes", + "credits": "/movie/%s/credits", + "external_ids": "/movie/%s/external_ids", + "images": "/movie/%s/images", + "keywords": "/movie/%s/keywords", + "lists": "/movie/%s/lists", + "reviews": "/movie/%s/reviews", + "videos": "/movie/%s/videos", + "recommendations": "/movie/%s/recommendations", + "latest": "/movie/latest", + "now_playing": "/movie/now_playing", + "top_rated": "/movie/top_rated", + "upcoming": "/movie/upcoming", + "popular": "/movie/popular", + "search_movie": "/search/movie", + "similar": "/movie/%s/similar", + "external": "/find/%s", + "release_dates": "/movie/%s/release_dates", + "watch_providers": "/movie/%s/watch/providers", + "translations": "/movie/%s/translations", + "discover": "/discover/movie" + } + + def details( + self, + movie_id, + append_to_response="", + ): + """ + Get the primary information about a movie. + :param movie_id: + :param append_to_response: + :return: + """ + if append_to_response == "all": + append_to_response = "images,credits,alternative_titles,translations,external_ids" + elif append_to_response is None: + append_to_response = "alternative_titles,translations,external_ids" + return AsObj( + **self._call( + self._urls["details"] % movie_id, + "append_to_response=" + append_to_response, + ) + ) + + def alternative_titles(self, movie_id): + """ + Get all of the alternative titles for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls["alternative_titles"] % movie_id, "")) + + def changes(self, movie_id, start_date="", end_date="", page=1): + """ + Get all of the alternative titles for a movie. + You can query up to 14 days in a single query by using the start_date and end_date query parameters. + :param movie_id: + :param start_date: + :param end_date: + :param page: + :return: + """ + return self._get_obj( + self._call( + self._urls["changes"] % movie_id, + urlencode({ + "start_date": str(start_date), + "end_date": str(end_date), + "page": str(page) + }) + ), + "changes" + ) + + def credidiscoverts(self, movie_id): + """ + Get the cast and crew for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls["credits"] % movie_id, "")) + + def external_ids(self, movie_id): + """ + Get the external ids for a movie. + :param movie_id: + :return: + """ + return self._get_obj( + self._call(self._urls["external_ids"] % (str(movie_id)), ""), None + ) + + def images(self, movie_id, include_image_language=""): + """ + Get the images that belong to a movie. + Querying images with a language parameter will filter the results. + If you want to include a fallback language (especially useful for backdrops) you can use the include_image_language parameter. + This should be a comma seperated value like so: include_image_language=en,null. + :param movie_id: + :param include_image_language: + :return: + """ + return AsObj(**self._call(self._urls['images'] % movie_id, "include_image_language=" + include_image_language)) + + def keywords(self, movie_id): + """ + Get the keywords associated to a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls['keywords'] % movie_id, '')) + + def lists(self, movie_id, page=1): + """ + Get a list of lists that this movie belongs to. + :param movie_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["lists"] % movie_id, "page=" + str(page)) + ) + + def recommendations(self, movie_id, page=1): + """ + Get a list of recommended movies for a movie. + :param movie_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["recommendations"] % movie_id, "page=" + str(page)) + ) + + def release_dates(self, movie_id): + """ + Get the release date along with the certification for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls['release_dates'] % movie_id, '')) + + def reviews(self, movie_id, page=1): + """ + Get the user reviews for a movie. + :param movie_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["reviews"] % movie_id, "page=" + str(page)) + ) + + def videos(self, vid, page=1): + """ + Get the videos that have been added to a movie. + :param vid: + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["videos"] % vid, "page=" + str(page))) + + def latest(self): + """ + Get the most newly created movie. This is a live response and will continuously change. + :return: + """ + return AsObj(**self._call(self._urls["latest"], "")) + + def now_playing(self, page=1): + """ + Get a list of movies in theatres. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["now_playing"], "page=" + str(page))) + + def top_rated(self, page=1): + """ + Get the top rated movies on TMDb. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["top_rated"], "page=" + str(page))) + + def upcoming(self, page=1): + """ + Get a list of upcoming movies in theatres. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["upcoming"], "page=" + str(page))) + + def popular(self, page=1): + """ + Get a list of the current popular movies on TMDb. This list updates daily. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["popular"], "page=" + str(page))) + + def search(self, term, page=1): + """ + Search for movies. + :param term: + :param page: + :return: + """ + return self._get_obj( + self._call( + self._urls["search_movie"], + "query=" + quote(term) + "&page=" + str(page), + ) + ) + + def similar(self, movie_id, page=1): + """ + Get a list of similar movies. + :param movie_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["similar"] % movie_id, "page=" + str(page)) + ) + + def external(self, external_id, external_source): + """ + The find method makes it easy to search for objects in our database by an external id. For example, an IMDB ID. + :param external_id: str + :param external_source str + :return: + """ + warnings.warn("external method is deprecated use tmdbv3api.Find().find(external_id, external_source)", + DeprecationWarning) + return self._get_obj( + self._call( + self._urls["external"] % external_id, + "external_source=" + external_source, + ), + key=None, + ) + + def watch_providers(self, movie_id): + """ + Get the Watch Providers for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls["watch_providers"] % movie_id, "")) + + def translations(self, movie_id): + """ + Get the Watch Providers for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls["translations"] % movie_id, "")) + + def discover(self, page): + """ + Movie discover. + :param page: + :return: + """ + return AsObj(**self._call(self._urls["discover"], "page=" + str(page))) + + def credits(self, movie_id): + """ + Get the Credits for a movie. + :param movie_id: + :return: + """ + return AsObj(**self._call(self._urls["credits"] % movie_id, "")) diff --git a/app/media/tmdbv3api/objs/person.py b/app/media/tmdbv3api/objs/person.py new file mode 100644 index 0000000..ab93440 --- /dev/null +++ b/app/media/tmdbv3api/objs/person.py @@ -0,0 +1,150 @@ +from app.media.tmdbv3api.as_obj import AsObj +from app.media.tmdbv3api.tmdb import TMDb + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + + +class Person(TMDb): + _urls = { + "details": "/person/%s", + "changes": "/person/%s/changes", + "movie_credits": "/person/%s/movie_credits", + "tv_credits": "/person/%s/tv_credits", + "combined_credits": "/person/%s/combined_credits", + "external_ids": "/person/%s/external_ids", + "images": "/person/%s/images", + "tagged_images": "/person/%s/tagged_images", + "translations": "/person/%s/translations", + "latest": "/person/latest", + "popular": "/person/popular", + } + + def details( + self, + person_id, + append_to_response="combined_credits,translations,external_ids", + ): + """ + Get the primary person details by id. + :param person_id: + :param append_to_response: + :return: + """ + return AsObj( + **self._call( + self._urls["details"] % person_id, + "append_to_response=" + append_to_response, + ) + ) + + def changes(self, person_id, start_date="", end_date="", page=1): + """ + Get the changes for a person. By default only the last 24 hours are returned. + You can query up to 14 days in a single query by using the start_date and end_date query parameters. + :param person_id: + :param start_date: + :param end_date: + :param page: + :return: + """ + return self._get_obj( + self._call( + self._urls["changes"] % person_id, + urlencode({ + "start_date": str(start_date), + "end_date": str(end_date), + "page": str(page) + }) + ), + "changes" + ) + + def movie_credits(self, person_id): + """ + Get the movie credits for a person. + :param person_id: + :return: + """ + return self._get_obj( + self._call( + self._urls["movie_credits"] % person_id, + "" + ), + "cast" + ) + + def tv_credits(self, person_id): + """ + Get the TV show credits for a person. + :param person_id: + :return: + """ + return self._get_obj( + self._call( + self._urls["tv_credits"] % person_id, + "" + ), + "cast" + ) + + def combined_credits(self, person_id): + """ + Get the movie and TV credits together in a single response. + :param person_id: + :return: + """ + return AsObj(**self._call(self._urls["combined_credits"] % person_id, "")) + + def external_ids(self, person_id): + """ + Get the external ids for a person. + :param person_id: + :return: + """ + return self._get_obj( + self._call(self._urls["external_ids"] % (str(person_id)), ""), None + ) + + def images(self, person_id): + """ + Get the images for a person. + :param person_id: + :param include_image_language: + :return: + """ + return AsObj(**self._call(self._urls['images'] % person_id, "")) + + def tagged_images(self, person_id): + """ + Get the images that this person has been tagged in. + :param person_id: + :param include_image_language: + :return: + """ + return AsObj(**self._call(self._urls['tagged_images'] % person_id, "")) + + def translations(self, person_id): + """ + Get a list of translations that have been created for a person. + :param person_id: + :return: + """ + return AsObj(**self._call(self._urls["translations"] % person_id, "")) + + def latest(self): + """ + Get the most newly created person. This is a live response and will continuously change. + :return: + """ + return AsObj(**self._call(self._urls["latest"], "")) + + def popular(self, page=1): + """ + Get the list of popular people on TMDB. This list updates daily. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["popular"], "page=" + str(page))) diff --git a/app/media/tmdbv3api/objs/search.py b/app/media/tmdbv3api/objs/search.py new file mode 100644 index 0000000..36516ec --- /dev/null +++ b/app/media/tmdbv3api/objs/search.py @@ -0,0 +1,74 @@ +from app.media.tmdbv3api.tmdb import TMDb + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + + +class Search(TMDb): + _urls = { + "companies": "/search/company", + "collections": "/search/collection", + "keywords": "/search/keyword", + "movies": "/search/movie", + "multi": "/search/multi", + "people": "/search/person", + "tv_shows": "/search/tv", + } + + def companies(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["companies"], urlencode(params))) + + def collections(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["collections"], urlencode(params))) + + def keywords(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["keywords"], urlencode(params))) + + def movies(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["movies"], urlencode(params))) + + def multi(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["multi"], urlencode(params))) + + def people(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["people"], urlencode(params))) + + def tv_shows(self, params): + """ + Search for movies. + :param params: + :return: + """ + return self._get_obj(self._call(self._urls["tv_shows"], urlencode(params))) diff --git a/app/media/tmdbv3api/objs/trending.py b/app/media/tmdbv3api/objs/trending.py new file mode 100644 index 0000000..e181b3f --- /dev/null +++ b/app/media/tmdbv3api/objs/trending.py @@ -0,0 +1,77 @@ +from app.media.tmdbv3api.tmdb import TMDb + + +class Trending(TMDb): + _urls = {"trending": "/trending/%s/%s"} + + def _trending(self, media_type="all", time_window="day", page=1): + return self._get_obj( + self._call( + self._urls["trending"] % (media_type, time_window), + "page=%s" % page + ) + ) + + def all_day(self, page=1): + """ + Get all daily trending + :param page: int + :return: + """ + return self._trending(media_type="all", time_window="day", page=page) + + def all_week(self, page=1): + """ + Get all weekly trending + :param page: int + :return: + """ + return self._trending(media_type="all", time_window="week", page=page) + + def movie_day(self, page=1): + """ + Get movie daily trending + :param page: int + :return: + """ + return self._trending(media_type="movie", time_window="day", page=page) + + def movie_week(self, page=1): + """ + Get movie weekly trending + :param page: int + :return: + """ + return self._trending(media_type="movie", time_window="week", page=page) + + def tv_day(self, page=1): + """ + Get tv daily trending + :param page: int + :return: + """ + return self._trending(media_type="tv", time_window="day", page=page) + + def tv_week(self, page=1): + """ + Get tv weekly trending + :param page: int + :return: + """ + return self._trending(media_type="tv", time_window="week", page=page) + + def person_day(self, page=1): + """ + Get person daily trending + :param page: int + :return: + """ + return self._trending(media_type="person", time_window="day", page=page) + + def person_week(self, page=1): + """ + Get person weekly trending + :param page: int + :return: + """ + return self._trending(media_type="person", time_window="week", page=page) diff --git a/app/media/tmdbv3api/objs/tv.py b/app/media/tmdbv3api/objs/tv.py new file mode 100644 index 0000000..fb9d8ed --- /dev/null +++ b/app/media/tmdbv3api/objs/tv.py @@ -0,0 +1,241 @@ +from app.media.tmdbv3api.as_obj import AsObj +from app.media.tmdbv3api.tmdb import TMDb + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + + +class TV(TMDb): + _urls = { + "details": "/tv/%s", + "latest": "/tv/latest", + "search_tv": "/search/tv", + "popular": "/tv/popular", + "top_rated": "/tv/top_rated", + "similar": "/tv/%s/similar", + "recommendations": "/tv/%s/recommendations", + "videos": "/tv/%s/videos", + "airing_today": "/tv/airing_today", + "on_the_air": "/tv/on_the_air", + "screened_theatrically": "/tv/%s/screened_theatrically", + "external_ids": "/tv/%s/external_ids", + "reviews": "/tv/%s/reviews", + "keywords": "/tv/%s/keywords", + "watch_providers": "/tv/%s/watch/providers", + "translations": "/tv/%s/translations", + "season_details": "/tv/%s/season/%s", + "alternative_titles": "/tv/%s/alternative_titles", + "credits": "/tv/%s/credits", + "discover": "/discover/tv", + "images": "/tv/%s/images" + } + + def details( + self, show_id, append_to_response="" + ): + """ + Get the primary TV show details by id. + :param show_id: + :param append_to_response: + :return: + """ + if append_to_response == "all": + append_to_response = "images,credits,alternative_titles,translations,external_ids" + elif append_to_response is None: + append_to_response = "alternative_titles,translations,external_ids" + return AsObj( + **self._call( + self._urls["details"] % str(show_id), + "append_to_response=" + append_to_response, + ) + ) + + def latest(self): + """ + Get the most newly created TV show. This is a live response and will continuously change. + :return: + """ + return AsObj(**self._call(self._urls["latest"], "")) + + def search(self, term, page=1): + """ + Search for a TV show. + :param term: + :param page: + :return: + """ + return self._get_obj( + self._call( + self._urls["search_tv"], "query=" + quote(term) + "&page=" + str(page) + ) + ) + + def similar(self, tv_id, page=1): + """ + Get the primary TV show details by id. + :param tv_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["similar"] % str(tv_id), "page=" + str(page)) + ) + + def popular(self, page=1): + """ + Get a list of the current popular TV shows on TMDb. This list updates daily. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["popular"], "page=" + str(page))) + + def top_rated(self, page=1): + """ + Get a list of the top rated TV shows on TMDb. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["top_rated"], "page=" + str(page))) + + def recommendations(self, tv_id, page=1): + """ + Get the list of TV show recommendations for this item. + :param tv_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["recommendations"] % tv_id, "page=" + str(page)) + ) + + def videos(self, tv_id, page=1): + """ + Get the videos that have been added to a TV show. + :param tv_id: + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["videos"] % tv_id, "page=" + str(page)) + ) + + def airing_today(self, page=1): + """ + Get a list of TV shows that are airing today. + This query is purely day based as we do not currently support airing times. + :param page: + :return: + """ + return self._get_obj( + self._call(self._urls["airing_today"], "page=" + str(page)) + ) + + def on_the_air(self, page=1): + """ + Get a list of shows that are currently on the air. + :param page: + :return: + """ + return self._get_obj(self._call(self._urls["on_the_air"], "page=" + str(page))) + + def screened_theatrically(self, tv_id): + """ + Get a list of seasons or episodes that have been screened in a film festival or theatre. + :param tv_id: + :return: + """ + return self._get_obj( + self._call(self._urls["screened_theatrically"] % tv_id, "") + ) + + def external_ids(self, vid): + """ + Get the external ids for a TV show. + :param vid: + :return: + """ + return self._get_obj( + self._call(self._urls["external_ids"] % (str(vid)), ""), None + ) + + def keywords(self, tv_id): + """ + Get the keywords that have been added to a TV show. + :param tv_id: int + :return: + """ + return self._get_obj(self._call(self._urls["keywords"] % tv_id, "")) + + def reviews(self, tv_id, page=1): + """ + Get the reviews for a TV show. + :param page: int + :param tv_id: int + :return: + """ + return self._get_obj( + self._call(self._urls["reviews"] % tv_id, "page=" + str(page)) + ) + + def watch_providers(self, tv_id): + """ + Get the Watch Providers for a TV show. + :param tv_id: + :return: + """ + return AsObj(**self._call(self._urls["watch_providers"] % tv_id, "")) + + def translations(self, tv_id): + """ + Get the translations for a TV show. + :param tv_id: tvid + :return: + """ + return AsObj(**self._call(self._urls["translations"] % tv_id, "")) + + def season_details(self, tv_id, season_number): + """ + Get the Season Detail for a TV show. + :param tv_id: tmdbid + :param season_number: season number + :return: + """ + return AsObj(**self._call(self._urls["season_details"] % (tv_id, season_number), "")) + + def alternative_titles(self, tv_id): + """ + Get all of the alternative titles for a TV show. + :param tv_id: + :return: + """ + return AsObj(**self._call(self._urls["alternative_titles"] % tv_id, "")) + + def credits(self, tv_id): + """ + Get the cast and crew for a TV show. + :param tv_id: + :return: + """ + return AsObj(**self._call(self._urls["credits"] % tv_id, "")) + + def discover(self, page): + """ + Tv discover. + :param page: + :return: + """ + return AsObj(**self._call(self._urls["discover"], "page=" + str(page))) + + def images(self, tv_id, include_image_language=""): + """ + Get the images that belong to a movie. + Querying images with a language parameter will filter the results. + If you want to include a fallback language (especially useful for backdrops) you can use the include_image_language parameter. + This should be a comma seperated value like so: include_image_language=en,null. + :param tv_id: + :param include_image_language: + :return: + """ + return AsObj(**self._call(self._urls['images'] % tv_id, "include_image_language=" + include_image_language)) diff --git a/app/media/tmdbv3api/tmdb.py b/app/media/tmdbv3api/tmdb.py new file mode 100644 index 0000000..04a19e8 --- /dev/null +++ b/app/media/tmdbv3api/tmdb.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +import logging +import os +import time +from functools import lru_cache + +import requests +import requests.exceptions + +from .as_obj import AsObj +from .exceptions import TMDbException + +logger = logging.getLogger(__name__) + + +class TMDb(object): + TMDB_API_KEY = "TMDB_API_KEY" + TMDB_LANGUAGE = "TMDB_LANGUAGE" + TMDB_WAIT_ON_RATE_LIMIT = "TMDB_WAIT_ON_RATE_LIMIT" + TMDB_DEBUG_ENABLED = "TMDB_DEBUG_ENABLED" + TMDB_CACHE_ENABLED = "TMDB_CACHE_ENABLED" + TMDB_PROXIES = "TMDB_PROXIES" + TMDB_DOMAIN = "TMDB_DOMAIN" + REQUEST_CACHE_MAXSIZE = 256 + + def __init__(self, obj_cached=True, session=None): + self._session = requests.Session() if session is None else session + self._remaining = 40 + self._reset = None + self.obj_cached = obj_cached + if os.environ.get(self.TMDB_LANGUAGE) is None: + os.environ[self.TMDB_LANGUAGE] = "zh-CN" + if not os.environ.get(self.TMDB_DOMAIN): + os.environ[self.TMDB_DOMAIN] = "https://api.themoviedb.org/3" + + @property + def page(self): + return os.environ["page"] + + @property + def total_results(self): + return os.environ["total_results"] + + @property + def total_pages(self): + return os.environ["total_pages"] + + @property + def api_key(self): + return os.environ.get(self.TMDB_API_KEY) + + @property + def domain(self): + return os.environ.get(self.TMDB_DOMAIN) + + @domain.setter + def domain(self, domain): + if domain: + if not str(domain).startswith('http'): + domain = "https://%s" % domain + if not str(domain).endswith('/3'): + domain = "%s/3" % domain + os.environ[self.TMDB_DOMAIN] = str(domain) + else: + os.environ[self.TMDB_DOMAIN] = '' + + @property + def proxies(self): + return os.environ.get(self.TMDB_PROXIES) + + @proxies.setter + def proxies(self, proxies): + if proxies: + proxies_strs = [] + for key, value in proxies.items(): + if not value: + continue + proxies_strs.append("'%s': '%s'" % (key, value)) + if proxies_strs: + os.environ[self.TMDB_PROXIES] = "{%s}" % ",".join(proxies_strs) + else: + os.environ[self.TMDB_PROXIES] = 'None' + + @api_key.setter + def api_key(self, api_key): + os.environ[self.TMDB_API_KEY] = str(api_key) + + @property + def language(self): + return os.environ.get(self.TMDB_LANGUAGE) + + @language.setter + def language(self, language): + os.environ[self.TMDB_LANGUAGE] = language + + @property + def wait_on_rate_limit(self): + if os.environ.get(self.TMDB_WAIT_ON_RATE_LIMIT) == "False": + return False + else: + return True + + @wait_on_rate_limit.setter + def wait_on_rate_limit(self, wait_on_rate_limit): + os.environ[self.TMDB_WAIT_ON_RATE_LIMIT] = str(wait_on_rate_limit) + + @property + def debug(self): + if os.environ.get(self.TMDB_DEBUG_ENABLED) == "True": + return True + else: + return False + + @debug.setter + def debug(self, debug): + os.environ[self.TMDB_DEBUG_ENABLED] = str(debug) + + @property + def cache(self): + if os.environ.get(self.TMDB_CACHE_ENABLED) == "False": + return False + else: + return True + + @cache.setter + def cache(self, cache): + os.environ[self.TMDB_CACHE_ENABLED] = str(cache) + + @staticmethod + def _get_obj(result, key="results", all_details=False): + if "success" in result and result["success"] is False: + raise TMDbException(result["status_message"]) + if all_details is True or key is None: + return AsObj(**result) + else: + return [AsObj(**res) for res in result[key]] + + @staticmethod + @lru_cache(maxsize=REQUEST_CACHE_MAXSIZE) + def cached_request(method, url, data, proxies): + return requests.request(method, url, data=data, proxies=eval(proxies), verify=False, timeout=10) + + def cache_clear(self): + return self.cached_request.cache_clear() + + def _call( + self, action, append_to_response, call_cached=True, method="GET", data=None + ): + if self.api_key is None or self.api_key == "": + raise TMDbException("No API key found.") + + url = "%s%s?api_key=%s&%s&language=%s" % ( + self.domain, + action, + self.api_key, + append_to_response, + self.language, + ) + + if self.cache and self.obj_cached and call_cached and method != "POST": + req = self.cached_request(method, url, data, self.proxies) + else: + req = self._session.request(method, url, data=data, proxies=eval(self.proxies), timeout=10, verify=False) + + headers = req.headers + + if "X-RateLimit-Remaining" in headers: + self._remaining = int(headers["X-RateLimit-Remaining"]) + + if "X-RateLimit-Reset" in headers: + self._reset = int(headers["X-RateLimit-Reset"]) + + if self._remaining < 1: + current_time = int(time.time()) + sleep_time = self._reset - current_time + + if self.wait_on_rate_limit: + logger.warning("Rate limit reached. Sleeping for: %d" % sleep_time) + time.sleep(abs(sleep_time)) + self._call(action, append_to_response, call_cached, method, data) + else: + raise TMDbException( + "Rate limit reached. Try again in %d seconds." % sleep_time + ) + + json = req.json() + + if "page" in json: + os.environ["page"] = str(json["page"]) + + if "total_results" in json: + os.environ["total_results"] = str(json["total_results"]) + + if "total_pages" in json: + os.environ["total_pages"] = str(json["total_pages"]) + + if self.debug: + logger.info(json) + logger.info(self.cached_request.cache_info()) + + if "errors" in json: + raise TMDbException(json["errors"]) + + return json diff --git a/app/mediaserver/__init__.py b/app/mediaserver/__init__.py new file mode 100644 index 0000000..7e2ca7b --- /dev/null +++ b/app/mediaserver/__init__.py @@ -0,0 +1,2 @@ +from .media_server import MediaServer +from .webhook_event import WebhookEvent diff --git a/app/mediaserver/client/__init__.py b/app/mediaserver/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/mediaserver/client/_base.py b/app/mediaserver/client/_base.py new file mode 100644 index 0000000..7674beb --- /dev/null +++ b/app/mediaserver/client/_base.py @@ -0,0 +1,108 @@ +from abc import ABCMeta, abstractmethod + + +class _IMediaClient(metaclass=ABCMeta): + + @abstractmethod + def match(self, ctype): + """ + 匹配实例 + """ + pass + + @abstractmethod + def get_status(self): + """ + 检查连通性 + """ + pass + + @abstractmethod + def get_user_count(self): + """ + 获得用户数量 + """ + pass + + @abstractmethod + def get_activity_log(self, num): + """ + 获取Emby活动记录 + """ + pass + + @abstractmethod + def get_medias_count(self): + """ + 获得电影、电视剧、动漫媒体数量 + :return: MovieCount SeriesCount SongCount + """ + pass + + @abstractmethod + def get_movies(self, title, year): + """ + 根据标题和年份,检查电影是否在存在,存在则返回列表 + :param title: 标题 + :param year: 年份,可以为空,为空时不按年份过滤 + :return: 含title、year属性的字典列表 + """ + pass + + @abstractmethod + def get_no_exists_episodes(self, meta_info, season, total_num): + """ + 根据标题、年份、季、总集数,查询缺少哪几集 + :param meta_info: 已识别的需要查询的媒体信息 + :param season: 季号,数字 + :param total_num: 该季的总集数 + :return: 该季不存在的集号列表 + """ + pass + + @abstractmethod + def get_image_by_id(self, item_id, image_type): + """ + 根据ItemId查询图片地址 + :param item_id: 在服务器中的ID + :param image_type: 图片的类弄地,poster或者backdrop等 + :return: 图片对应在TMDB中的URL + """ + pass + + @abstractmethod + def refresh_root_library(self): + """ + 刷新整个媒体库 + """ + pass + + @abstractmethod + def refresh_library_by_items(self, items): + """ + 按类型、名称、年份来刷新媒体库 + :param items: 已识别的需要刷新媒体库的媒体信息列表 + """ + pass + + @abstractmethod + def get_libraries(self): + """ + 获取媒体服务器所有媒体库列表 + """ + pass + + @abstractmethod + def get_items(self, parent): + """ + 获取媒体库中的所有媒体 + :param parent: 上一级的ID + """ + pass + + @abstractmethod + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/client/emby.py b/app/mediaserver/client/emby.py new file mode 100644 index 0000000..8da8720 --- /dev/null +++ b/app/mediaserver/client/emby.py @@ -0,0 +1,492 @@ +import os +import re + +import log +from config import Config +from app.mediaserver.client._base import _IMediaClient +from app.utils import RequestUtils, SystemUtils, ExceptionUtils +from app.utils.types import MediaType, MediaServerType + + +class Emby(_IMediaClient): + schema = "emby" + server_type = MediaServerType.EMBY.value + _client_config = {} + + _apikey = None + _host = None + _user = None + _libraries = [] + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('emby') + self.init_config() + + def init_config(self): + if self._client_config: + self._host = self._client_config.get('host') + if self._host: + if not self._host.startswith('http'): + self._host = "http://" + self._host + if not self._host.endswith('/'): + self._host = self._host + "/" + self._apikey = self._client_config.get('api_key') + if self._host and self._apikey: + self._libraries = self.__get_emby_librarys() + self._user = self.get_admin_user() + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.server_type] else False + + def get_status(self): + """ + 测试连通性 + """ + return True if self.get_medias_count() else False + + def __get_emby_librarys(self): + """ + 获取Emby媒体库列表 + """ + if not self._host or not self._apikey: + return [] + req_url = "%semby/Library/SelectableMediaFolders?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return res.json() + else: + log.error(f"【{self.server_type}】Library/SelectableMediaFolders 未获取到返回数据") + return [] + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Library/SelectableMediaFolders 出错:" + str(e)) + return [] + + def get_admin_user(self): + """ + 获得管理员用户 + """ + if not self._host or not self._apikey: + return None + req_url = "%sUsers?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + users = res.json() + for user in users: + if user.get("Policy", {}).get("IsAdministrator"): + return user.get("Id") + else: + log.error(f"【{self.server_type}】Users 未获取到返回数据") + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users出错:" + str(e)) + return None + + def get_user_count(self): + """ + 获得用户数量 + """ + if not self._host or not self._apikey: + return 0 + req_url = "%semby/Users/Query?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return res.json().get("TotalRecordCount") + else: + log.error(f"【{self.server_type}】Users/Query 未获取到返回数据") + return 0 + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users/Query出错:" + str(e)) + return 0 + + def get_activity_log(self, num): + """ + 获取Emby活动记录 + """ + if not self._host or not self._apikey: + return [] + req_url = "%semby/System/ActivityLog/Entries?api_key=%s&" % (self._host, self._apikey) + ret_array = [] + try: + res = RequestUtils().get_res(req_url) + if res: + ret_json = res.json() + items = ret_json.get('Items') + for item in items: + if item.get("Type") == "AuthenticationSucceeded": + event_type = "LG" + event_date = SystemUtils.get_local_time(item.get("Date")) + event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview")) + activity = {"type": event_type, "event": event_str, "date": event_date} + ret_array.append(activity) + if item.get("Type") == "VideoPlayback": + event_type = "PL" + event_date = SystemUtils.get_local_time(item.get("Date")) + event_str = item.get("Name") + activity = {"type": event_type, "event": event_str, "date": event_date} + ret_array.append(activity) + else: + log.error(f"【{self.server_type}】System/ActivityLog/Entries 未获取到返回数据") + return [] + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接System/ActivityLog/Entries出错:" + str(e)) + return [] + return ret_array[:num] + + def get_medias_count(self): + """ + 获得电影、电视剧、动漫媒体数量 + :return: MovieCount SeriesCount SongCount + """ + if not self._host or not self._apikey: + return {} + req_url = "%semby/Items/Counts?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return res.json() + else: + log.error(f"【{self.server_type}】Items/Counts 未获取到返回数据") + return {} + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items/Counts出错:" + str(e)) + return {} + + def __get_emby_series_id_by_name(self, name, year): + """ + 根据名称查询Emby中剧集的SeriesId + :param name: 标题 + :param year: 年份 + :return: None 表示连不通,""表示未找到,找到返回ID + """ + if not self._host or not self._apikey: + return None + req_url = "%semby/Items?IncludeItemTypes=Series&Fields=ProductionYear&StartIndex=0&Recursive=true&SearchTerm=%s&Limit=10&IncludeSearchTypes=false&api_key=%s" % ( + self._host, name, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + res_items = res.json().get("Items") + if res_items: + for res_item in res_items: + if res_item.get('Name') == name and ( + not year or str(res_item.get('ProductionYear')) == str(year)): + return res_item.get('Id') + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items出错:" + str(e)) + return None + return "" + + def get_movies(self, title, year=None): + """ + 根据标题和年份,检查电影是否在Emby中存在,存在则返回列表 + :param title: 标题 + :param year: 年份,可以为空,为空时不按年份过滤 + :return: 含title、year属性的字典列表 + """ + if not self._host or not self._apikey: + return None + req_url = "%semby/Items?IncludeItemTypes=Movie&Fields=ProductionYear&StartIndex=0&Recursive=true&SearchTerm=%s&Limit=10&IncludeSearchTypes=false&api_key=%s" % ( + self._host, title, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + res_items = res.json().get("Items") + if res_items: + ret_movies = [] + for res_item in res_items: + if res_item.get('Name') == title and ( + not year or str(res_item.get('ProductionYear')) == str(year)): + ret_movies.append( + {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + return ret_movies + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items出错:" + str(e)) + return None + return [] + + def __get_emby_tv_episodes(self, title, year, tmdb_id=None, season=None): + """ + 根据标题和年份和季,返回Emby中的剧集列表 + :param title: 标题 + :param year: 年份,可以为空,为空时不按年份过滤 + :param tmdb_id: TMDBID + :param season: 季 + :return: 集号的列表 + """ + if not self._host or not self._apikey: + return None + # 电视剧 + item_id = self.__get_emby_series_id_by_name(title, year) + if item_id is None: + return None + if not item_id: + return [] + # 验证tmdbid是否相同 + item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb") + if tmdb_id and item_tmdbid: + if str(tmdb_id) != str(item_tmdbid): + return [] + # /Shows/Id/Episodes 查集的信息 + if not season: + season = 1 + req_url = "%semby/Shows/%s/Episodes?Season=%s&IsMissing=false&api_key=%s" % ( + self._host, item_id, season, self._apikey) + try: + res_json = RequestUtils().get_res(req_url) + if res_json: + res_items = res_json.json().get("Items") + exists_episodes = [] + for res_item in res_items: + exists_episodes.append(int(res_item.get("IndexNumber"))) + return exists_episodes + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Shows/Id/Episodes出错:" + str(e)) + return None + return [] + + def get_no_exists_episodes(self, meta_info, season, total_num): + """ + 根据标题、年份、季、总集数,查询Emby中缺少哪几集 + :param meta_info: 已识别的需要查询的媒体信息 + :param season: 季号,数字 + :param total_num: 该季的总集数 + :return: 该季不存在的集号列表 + """ + if not self._host or not self._apikey: + return None + exists_episodes = self.__get_emby_tv_episodes(meta_info.title, meta_info.year, meta_info.tmdb_id, season) + if not isinstance(exists_episodes, list): + return None + total_episodes = [episode for episode in range(1, total_num + 1)] + return list(set(total_episodes).difference(set(exists_episodes))) + + def get_image_by_id(self, item_id, image_type): + """ + 根据ItemId从Emby查询图片地址 + :param item_id: 在Emby中的ID + :param image_type: 图片的类弄地,poster或者backdrop等 + :return: 图片对应在TMDB中的URL + """ + if not self._host or not self._apikey: + return None + req_url = "%semby/Items/%s/RemoteImages?api_key=%s" % (self._host, item_id, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + images = res.json().get("Images") + for image in images: + if image.get("ProviderName") == "TheMovieDb" and image.get("Type") == image_type: + return image.get("Url") + else: + log.error(f"【{self.server_type}】Items/RemoteImages 未获取到返回数据") + return None + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items/Id/RemoteImages出错:" + str(e)) + return None + return None + + def __refresh_emby_library_by_id(self, item_id): + """ + 通知Emby刷新一个项目的媒体库 + """ + if not self._host or not self._apikey: + return False + req_url = "%semby/Items/%s/Refresh?Recursive=true&api_key=%s" % (self._host, item_id, self._apikey) + try: + res = RequestUtils().post_res(req_url) + if res: + return True + else: + log.info(f"【{self.server_type}】刷新媒体库对象 {item_id} 失败,无法连接Emby!") + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items/Id/Refresh出错:" + str(e)) + return False + return False + + def refresh_root_library(self): + """ + 通知Emby刷新整个媒体库 + """ + if not self._host or not self._apikey: + return False + req_url = "%semby/Library/Refresh?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().post_res(req_url) + if res: + return True + else: + log.info(f"【{self.server_type}】刷新媒体库失败,无法连接Emby!") + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Library/Refresh出错:" + str(e)) + return False + return False + + def refresh_library_by_items(self, items): + """ + 按类型、名称、年份来刷新媒体库 + :param items: 已识别的需要刷新媒体库的媒体信息列表 + """ + if not items: + return + # 收集要刷新的媒体库信息 + log.info(f"【{self.server_type}】开始刷新Emby媒体库...") + library_ids = [] + for item in items: + if not item: + continue + library_id = self.__get_emby_library_id_by_item(item) + if library_id and library_id not in library_ids: + library_ids.append(library_id) + # 开始刷新媒体库 + if "/" in library_ids: + self.refresh_root_library() + return + for library_id in library_ids: + if library_id != "/": + self.__refresh_emby_library_by_id(library_id) + log.info(f"【{self.server_type}】Emby媒体库刷新完成") + + def __get_emby_library_id_by_item(self, item): + """ + 根据媒体信息查询在哪个媒体库,返回要刷新的位置的ID + :param item: 由title、year、type组成的字典 + """ + if not item.get("title") or not item.get("year") or not item.get("type"): + return None + if item.get("type") == MediaType.TV: + item_id = self.__get_emby_series_id_by_name(item.get("title"), item.get("year")) + if item_id: + # 存在电视剧,则直接刷新这个电视剧就行 + return item_id + else: + if self.get_movies(item.get("title"), item.get("year")): + # 已存在,不用刷新 + return None + # 查找需要刷新的媒体库ID + for library in self._libraries: + # 找同级路径最多的媒体库(要求容器内映射路径与实际一致) + max_equal_path_id = None + max_path_len = 0 + equal_path_num = 0 + for folder in library.get("SubFolders"): + path_list = re.split(pattern='/+|\\\\+', string=folder.get("Path")) + if item.get("category") != path_list[-1]: + continue + try: + path_len = len(os.path.commonpath([item.get("target_path"), folder.get("Path")])) + if path_len >= max_path_len: + max_path_len = path_len + max_equal_path_id = folder.get("Id") + equal_path_num += 1 + except Exception as err: + ExceptionUtils.exception_traceback(err) + continue + if max_equal_path_id: + return max_equal_path_id if equal_path_num == 1 else library.get("Id") + # 如果找不到,只要路径中有分类目录名就命中 + for folder in library.get("SubFolders"): + if folder.get("Path") and re.search(r"[/\\]%s" % item.get("category"), folder.get("Path")): + return library.get("Id") + # 刷新根目录 + return "/" + + def get_libraries(self): + """ + 获取媒体服务器所有媒体库列表 + """ + if self._host and self._apikey: + self._libraries = self.__get_emby_librarys() + libraries = [] + for library in self._libraries: + libraries.append({"id": library.get("Id"), "name": library.get("Name")}) + return libraries + + def get_iteminfo(self, itemid): + """ + 获取单个项目详情 + """ + if not itemid: + return {} + if not self._host or not self._apikey: + return {} + req_url = "%semby/Users/%s/Items/%s?api_key=%s" % (self._host, self._user, itemid, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + return res.json() + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {} + + def get_items(self, parent): + """ + 获取媒体服务器所有媒体库列表 + """ + if not parent: + yield {} + if not self._host or not self._apikey: + yield {} + req_url = "%semby/Users/%s/Items?ParentId=%s&api_key=%s" % (self._host, self._user, parent, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + results = res.json().get("Items") or [] + for result in results: + if not result: + continue + if result.get("Type") in ["Movie", "Series"]: + item_info = self.get_iteminfo(result.get("Id")) + yield {"id": result.get("Id"), + "library": item_info.get("ParentId"), + "type": item_info.get("Type"), + "title": item_info.get("Name"), + "originalTitle": item_info.get("OriginalTitle"), + "year": item_info.get("ProductionYear"), + "tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"), + "imdbid": item_info.get("ProviderIds", {}).get("Imdb"), + "path": item_info.get("Path"), + "json": str(item_info)} + elif "Folder" in result.get("Type"): + for item in self.get_items(parent=result.get('Id')): + yield item + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users/Items出错:" + str(e)) + yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + if not self._host or not self._apikey: + return [] + playing_sessions = [] + req_url = "%semby/Sessions?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + sessions = res.json() + for session in sessions: + if session.get("NowPlayingItem"): + playing_sessions.append(session) + return playing_sessions + except Exception as e: + ExceptionUtils.exception_traceback(e) + return [] diff --git a/app/mediaserver/client/jellyfin.py b/app/mediaserver/client/jellyfin.py new file mode 100644 index 0000000..4d511fd --- /dev/null +++ b/app/mediaserver/client/jellyfin.py @@ -0,0 +1,424 @@ +import re + +import log +from config import Config +from app.mediaserver.client._base import _IMediaClient +from app.utils.types import MediaServerType +from app.utils import RequestUtils, SystemUtils, ExceptionUtils + + +class Jellyfin(_IMediaClient): + schema = "jellyfin" + server_type = MediaServerType.JELLYFIN.value + _client_config = {} + + _apikey = None + _host = None + _user = None + _libraries = [] + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('jellyfin') + self.init_config() + + def init_config(self): + if self._client_config: + self._host = self._client_config.get('host') + if self._host: + if not self._host.startswith('http'): + self._host = "http://" + self._host + if not self._host.endswith('/'): + self._host = self._host + "/" + self._apikey = self._client_config.get('api_key') + if self._host and self._apikey: + self._user = self.get_admin_user() + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.server_type] else False + + def get_status(self): + """ + 测试连通性 + """ + return True if self.get_medias_count() else False + + def __get_jellyfin_librarys(self): + """ + 获取Jellyfin媒体库的信息 + """ + if not self._host or not self._apikey: + return [] + req_url = "%sLibrary/VirtualFolders?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return res.json() + else: + log.error(f"【{self.server_type}】Library/VirtualFolders 未获取到返回数据") + return [] + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Library/VirtualFolders 出错:" + str(e)) + return [] + + def get_user_count(self): + """ + 获得用户数量 + """ + if not self._host or not self._apikey: + return 0 + req_url = "%sUsers?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return len(res.json()) + else: + log.error(f"【{self.server_type}】Users 未获取到返回数据") + return 0 + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users出错:" + str(e)) + return 0 + + def get_admin_user(self): + """ + 获得管理员用户 + """ + if not self._host or not self._apikey: + return None + req_url = "%sUsers?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + users = res.json() + for user in users: + if user.get("Policy", {}).get("IsAdministrator"): + return user.get("Id") + else: + log.error(f"【{self.server_type}】Users 未获取到返回数据") + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users出错:" + str(e)) + return None + + def get_activity_log(self, num): + """ + 获取Jellyfin活动记录 + """ + if not self._host or not self._apikey: + return [] + req_url = "%sSystem/ActivityLog/Entries?api_key=%s&Limit=%s" % (self._host, self._apikey, num) + ret_array = [] + try: + res = RequestUtils().get_res(req_url) + if res: + ret_json = res.json() + items = ret_json.get('Items') + for item in items: + if item.get("Type") == "SessionStarted": + event_type = "LG" + event_date = re.sub(r'\dZ', 'Z', item.get("Date")) + event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview")) + activity = {"type": event_type, "event": event_str, + "date": SystemUtils.get_local_time(event_date)} + ret_array.append(activity) + if item.get("Type") == "VideoPlayback": + event_type = "PL" + event_date = re.sub(r'\dZ', 'Z', item.get("Date")) + activity = {"type": event_type, "event": item.get("Name"), + "date": SystemUtils.get_local_time(event_date)} + ret_array.append(activity) + else: + log.error(f"【{self.server_type}】System/ActivityLog/Entries 未获取到返回数据") + return [] + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接System/ActivityLog/Entries出错:" + str(e)) + return [] + return ret_array + + def get_medias_count(self): + """ + 获得电影、电视剧、动漫媒体数量 + :return: MovieCount SeriesCount SongCount + """ + if not self._host or not self._apikey: + return None + req_url = "%sItems/Counts?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + return res.json() + else: + log.error(f"【{self.server_type}】Items/Counts 未获取到返回数据") + return {} + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items/Counts出错:" + str(e)) + return {} + + def __get_jellyfin_series_id_by_name(self, name, year): + """ + 根据名称查询Jellyfin中剧集的SeriesId + """ + if not self._host or not self._apikey or not self._user: + return None + req_url = "%sUsers/%s/Items?api_key=%s&searchTerm=%s&IncludeItemTypes=Series&Limit=10&Recursive=true" % ( + self._host, self._user, self._apikey, name) + try: + res = RequestUtils().get_res(req_url) + if res: + res_items = res.json().get("Items") + if res_items: + for res_item in res_items: + if res_item.get('Name') == name and ( + not year or str(res_item.get('ProductionYear')) == str(year)): + return res_item.get('Id') + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items出错:" + str(e)) + return None + return "" + + def __get_jellyfin_season_id_by_name(self, name, year, season): + """ + 根据名称查询Jellyfin中剧集和季对应季的Id + """ + if not self._host or not self._apikey or not self._user: + return None, None + series_id = self.__get_jellyfin_series_id_by_name(name, year) + if series_id is None: + return None, None + if not series_id: + return "", "" + if not season: + season = 1 + req_url = "%sShows/%s/Seasons?api_key=%s&userId=%s" % ( + self._host, series_id, self._apikey, self._user) + try: + res = RequestUtils().get_res(req_url) + if res: + res_items = res.json().get("Items") + if res_items: + for res_item in res_items: + if int(res_item.get('IndexNumber')) == int(season): + return series_id, res_item.get('Id') + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Shows/Id/Seasons出错:" + str(e)) + return None, None + return "", "" + + def get_movies(self, title, year=None): + """ + 根据标题和年份,检查电影是否在Jellyfin中存在,存在则返回列表 + :param title: 标题 + :param year: 年份,为空则不过滤 + :return: 含title、year属性的字典列表 + """ + if not self._host or not self._apikey or not self._user: + return None + req_url = "%sUsers/%s/Items?api_key=%s&searchTerm=%s&IncludeItemTypes=Movie&Limit=10&Recursive=true" % ( + self._host, self._user, self._apikey, title) + try: + res = RequestUtils().get_res(req_url) + if res: + res_items = res.json().get("Items") + if res_items: + ret_movies = [] + for res_item in res_items: + if res_item.get('Name') == title and ( + not year or str(res_item.get('ProductionYear')) == str(year)): + ret_movies.append( + {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + return ret_movies + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items出错:" + str(e)) + return None + return [] + + def __get_jellyfin_tv_episodes(self, title, year=None, tmdb_id=None, season=None): + """ + 根据标题和年份和季,返回Jellyfin中的剧集列表 + :param title: 标题 + :param year: 年份,可以为空,为空时不按年份过滤 + :param tmdb_id: TMDBID + :param season: 季 + :return: 集号的列表 + """ + if not self._host or not self._apikey or not self._user: + return None + # 电视剧 + series_id, season_id = self.__get_jellyfin_season_id_by_name(title, year, season) + if series_id is None or season_id is None: + return None + if not series_id or not season_id: + return [] + # 验证tmdbid是否相同 + item_tmdbid = self.get_iteminfo(series_id).get("ProviderIds", {}).get("Tmdb") + if tmdb_id and item_tmdbid: + if str(tmdb_id) != str(item_tmdbid): + return [] + req_url = "%sShows/%s/Episodes?seasonId=%s&&userId=%s&isMissing=false&api_key=%s" % ( + self._host, series_id, season_id, self._user, self._apikey) + try: + res_json = RequestUtils().get_res(req_url) + if res_json: + res_items = res_json.json().get("Items") + exists_episodes = [] + for res_item in res_items: + exists_episodes.append(int(res_item.get("IndexNumber"))) + return exists_episodes + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Shows/Id/Episodes出错:" + str(e)) + return None + return [] + + def get_no_exists_episodes(self, meta_info, season, total_num): + """ + 根据标题、年份、季、总集数,查询Jellyfin中缺少哪几集 + :param meta_info: 已识别的需要查询的媒体信息 + :param season: 季号,数字 + :param total_num: 该季的总集数 + :return: 该季不存在的集号列表 + """ + if not self._host or not self._apikey: + return None + exists_episodes = self.__get_jellyfin_tv_episodes(meta_info.title, meta_info.year, meta_info.tmdb_id, season) + if not isinstance(exists_episodes, list): + return None + total_episodes = [episode for episode in range(1, total_num + 1)] + return list(set(total_episodes).difference(set(exists_episodes))) + + def get_image_by_id(self, item_id, image_type): + """ + 根据ItemId从Jellyfin查询图片地址 + :param item_id: 在Emby中的ID + :param image_type: 图片的类弄地,poster或者backdrop等 + :return: 图片对应在TMDB中的URL + """ + if not self._host or not self._apikey: + return None + req_url = "%sItems/%s/RemoteImages?api_key=%s" % (self._host, item_id, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res: + images = res.json().get("Images") + for image in images: + if image.get("ProviderName") == "TheMovieDb" and image.get("Type") == image_type: + return image.get("Url") + else: + log.error(f"【{self.server_type}】Items/RemoteImages 未获取到返回数据") + return None + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Items/Id/RemoteImages出错:" + str(e)) + return None + return None + + def refresh_root_library(self): + """ + 通知Jellyfin刷新整个媒体库 + """ + if not self._host or not self._apikey: + return False + req_url = "%sLibrary/Refresh?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().post_res(req_url) + if res: + return True + else: + log.info(f"【{self.server_type}】刷新媒体库失败,无法连接Jellyfin!") + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Library/Refresh出错:" + str(e)) + return False + + def refresh_library_by_items(self, items): + """ + 按类型、名称、年份来刷新媒体库,Jellyfin没有刷单个项目的API,这里直接刷新整库 + :param items: 已识别的需要刷新媒体库的媒体信息列表 + """ + # 没找到单项目刷新的对应的API,先按全库刷新 + if not items: + return False + if not self._host or not self._apikey: + return False + return self.refresh_root_library() + + def get_libraries(self): + """ + 获取媒体服务器所有媒体库列表 + """ + if self._host and self._apikey: + self._libraries = self.__get_jellyfin_librarys() + libraries = [] + for library in self._libraries: + libraries.append({"id": library.get("ItemId"), "name": library.get("Name")}) + return libraries + + def get_iteminfo(self, itemid): + """ + 获取单个项目详情 + """ + if not itemid: + return {} + if not self._host or not self._apikey: + return {} + req_url = "%sUsers/%s/Items/%s?api_key=%s" % ( + self._host, self._user, itemid, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + return res.json() + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {} + + def get_items(self, parent): + """ + 获取媒体服务器所有媒体库列表 + """ + if not parent: + yield {} + if not self._host or not self._apikey: + yield {} + req_url = "%sUsers/%s/Items?parentId=%s&api_key=%s" % (self._host, self._user, parent, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + results = res.json().get("Items") or [] + for result in results: + if not result: + continue + if result.get("Type") in ["Movie", "Series"]: + item_info = self.get_iteminfo(result.get("Id")) + yield {"id": result.get("Id"), + "library": item_info.get("ParentId"), + "type": item_info.get("Type"), + "title": item_info.get("Name"), + "originalTitle": item_info.get("OriginalTitle"), + "year": item_info.get("ProductionYear"), + "tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"), + "imdbid": item_info.get("ProviderIds", {}).get("Imdb"), + "path": item_info.get("Path"), + "json": str(item_info)} + elif "Folder" in result.get("Type"): + for item in self.get_items(result.get("Id")): + yield item + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【{self.server_type}】连接Users/Items出错:" + str(e)) + yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/client/plex.py b/app/mediaserver/client/plex.py new file mode 100644 index 0000000..abc9fe5 --- /dev/null +++ b/app/mediaserver/client/plex.py @@ -0,0 +1,216 @@ +from app.utils import ExceptionUtils +from app.utils.types import MediaServerType + +import log +from config import Config +from app.mediaserver.client._base import _IMediaClient +from plexapi.myplex import MyPlexAccount +from plexapi.server import PlexServer + + +class Plex(_IMediaClient): + schema = "plex" + server_type = MediaServerType.PLEX.value + _client_config = {} + + _host = None + _token = None + _username = None + _password = None + _servername = None + _plex = None + _libraries = [] + + def __init__(self, config=None): + if config: + self._client_config = config + else: + self._client_config = Config().get_config('plex') + self.init_config() + + def init_config(self): + if self._client_config: + self._host = self._client_config.get('host') + self._token = self._client_config.get('token') + if self._host: + if not self._host.startswith('http'): + self._host = "http://" + self._host + if not self._host.endswith('/'): + self._host = self._host + "/" + self._username = self._client_config.get('username') + self._password = self._client_config.get('password') + self._servername = self._client_config.get('servername') + if self._host and self._token: + try: + self._plex = PlexServer(self._host, self._token) + except Exception as e: + ExceptionUtils.exception_traceback(e) + self._plex = None + log.error(f"【{self.server_type}】Plex服务器连接失败:{str(e)}") + elif self._username and self._password and self._servername: + try: + self._plex = MyPlexAccount(self._username, self._password).resource(self._servername).connect() + except Exception as e: + ExceptionUtils.exception_traceback(e) + self._plex = None + log.error(f"【{self.server_type}】Plex服务器连接失败:{str(e)}") + + @classmethod + def match(cls, ctype): + return True if ctype in [cls.schema, cls.server_type] else False + + def get_status(self): + """ + 测试连通性 + """ + return True if self._plex else False + + @staticmethod + def get_user_count(**kwargs): + """ + 获得用户数量,Plex只能配置一个用户,固定返回1 + """ + return 1 + + def get_activity_log(self, num): + """ + 获取Plex活动记录 + """ + if not self._plex: + return [] + ret_array = [] + historys = self._plex.library.history(num) + for his in historys: + event_type = "PL" + event_date = his.viewedAt.strftime('%Y-%m-%d %H:%M:%S') + event_str = "开始播放 %s" % his.title + activity = {"type": event_type, "event": event_str, "date": event_date} + ret_array.append(activity) + if ret_array: + ret_array = sorted(ret_array, key=lambda x: x['date'], reverse=True) + return ret_array + + def get_medias_count(self): + """ + 获得电影、电视剧、动漫媒体数量 + :return: MovieCount SeriesCount SongCount + """ + if not self._plex: + return {} + sections = self._plex.library.sections() + MovieCount = SeriesCount = SongCount = 0 + for sec in sections: + if sec.type == "movie": + MovieCount += sec.totalSize + if sec.type == "show": + SeriesCount += sec.totalSize + if sec.type == "artist": + SongCount += sec.totalSize + return {"MovieCount": MovieCount, "SeriesCount": SeriesCount, "SongCount": SongCount, "EpisodeCount": 0} + + def get_movies(self, title, year=None): + """ + 根据标题和年份,检查电影是否在Plex中存在,存在则返回列表 + :param title: 标题 + :param year: 年份,为空则不过滤 + :return: 含title、year属性的字典列表 + """ + if not self._plex: + return None + ret_movies = [] + if year: + movies = self._plex.library.search(title=title, year=year, libtype="movie") + else: + movies = self._plex.library.search(title=title, libtype="movie") + for movie in movies: + ret_movies.append({'title': movie.title, 'year': movie.year}) + return ret_movies + + # 根据标题、年份、季、总集数,查询Plex中缺少哪几集 + def get_no_exists_episodes(self, meta_info, season, total_num): + """ + 根据标题、年份、季、总集数,查询Plex中缺少哪几集 + :param meta_info: 已识别的需要查询的媒体信息 + :param season: 季号,数字 + :param total_num: 该季的总集数 + :return: 该季不存在的集号列表 + """ + if not self._plex: + return None + exists_episodes = [] + video = self._plex.library.search(title=meta_info.title, year=meta_info.year, libtype="show") + if video: + for episode in video[0].episodes(): + if episode.seasonNumber == season: + exists_episodes.append(episode.index) + total_episodes = [episode for episode in range(1, total_num + 1)] + return list(set(total_episodes).difference(set(exists_episodes))) + + @staticmethod + def get_image_by_id(**kwargs): + """ + 根据ItemId从Plex查询图片地址,该函数Plex下不使用 + """ + return None + + def refresh_root_library(self): + """ + 通知Plex刷新整个媒体库 + """ + if not self._plex: + return False + return self._plex.library.update() + + def refresh_library_by_items(self, items): + """ + 按类型、名称、年份来刷新媒体库,未找到对应的API,直接刷整库 + """ + if not self._plex: + return False + return self._plex.library.update() + + def get_libraries(self): + """ + 获取媒体服务器所有媒体库列表 + """ + if not self._plex: + return [] + try: + self._libraries = self._plex.library.sections() + except Exception as err: + ExceptionUtils.exception_traceback(err) + return [] + libraries = [] + for library in self._libraries: + libraries.append({"id": library.key, "name": library.title}) + return libraries + + def get_items(self, parent): + """ + 获取媒体服务器所有媒体库列表 + """ + if not parent: + yield {} + if not self._plex: + yield {} + try: + section = self._plex.library.sectionByID(parent) + if section: + for item in section.all(): + if not item: + continue + yield {"id": item.key, + "library": item.librarySectionID, + "type": item.type, + "title": item.title, + "year": item.year, + "json": str(item.__dict__)} + except Exception as err: + ExceptionUtils.exception_traceback(err) + yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/media_server.py b/app/mediaserver/media_server.py new file mode 100644 index 0000000..d0e011d --- /dev/null +++ b/app/mediaserver/media_server.py @@ -0,0 +1,246 @@ +import threading + +import log +from app.conf import ModuleConf +from app.db import MediaDb +from app.helper import ProgressHelper, SubmoduleHelper +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import MediaServerType +from config import Config + +lock = threading.Lock() +server_lock = threading.Lock() + + +@singleton +class MediaServer: + _mediaserver_schemas = [] + _server_type = None + _server = None + mediadb = None + progress = None + + def __init__(self): + self._mediaserver_schemas = SubmoduleHelper.import_submodules( + 'app.mediaserver.client', + filter_func=lambda _, obj: hasattr(obj, 'schema') + ) + log.debug(f"【MediaServer】加载媒体服务器:{self._mediaserver_schemas}") + self.init_config() + + def init_config(self): + self.mediadb = MediaDb() + self.progress = ProgressHelper() + # 当前使用的媒体库服务器 + _type = Config().get_config('media').get('media_server') or 'emby' + self._server_type = ModuleConf.MEDIASERVER_DICT.get(_type) + self._server = None + + def __build_class(self, ctype, conf): + for mediaserver_schema in self._mediaserver_schemas: + try: + if mediaserver_schema.match(ctype): + return mediaserver_schema(conf) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + @property + def server(self): + with server_lock: + if not self._server: + self._server = self.__get_server(self._server_type) + return self._server + + def __get_server(self, ctype: MediaServerType, conf=None): + return self.__build_class(ctype=ctype.value, conf=conf) + + def get_type(self): + """ + 当前使用的媒体库服务器 + """ + return self._server_type + + def get_activity_log(self, limit): + """ + 获取媒体服务器的活动日志 + :param limit: 条数限制 + """ + if not self.server: + return [] + return self.server.get_activity_log(limit) + + def get_user_count(self): + """ + 获取媒体服务器的总用户数 + """ + if not self.server: + return 0 + return self.server.get_user_count() + + def get_medias_count(self): + """ + 获取媒体服务器各类型的媒体库 + :return: MovieCount SeriesCount SongCount + """ + if not self.server: + return None + return self.server.get_medias_count() + + def refresh_root_library(self): + """ + 刷新媒体服务器整个媒体库 + """ + if not self.server: + return + return self.server.refresh_root_library() + + def get_image_by_id(self, item_id, image_type): + """ + 根据ItemId从媒体服务器查询图片地址 + :param item_id: 在Emby中的ID + :param image_type: 图片的类弄地,poster或者backdrop等 + :return: 图片对应在TMDB中的URL + """ + if not self.server: + return None + return self.server.get_image_by_id(item_id, image_type) + + def get_no_exists_episodes(self, meta_info, + season_number, + episode_count): + """ + 根据标题、年份、季、总集数,查询媒体服务器中缺少哪几集 + :param meta_info: 已识别的需要查询的媒体信息 + :param season_number: 季号,数字 + :param episode_count: 该季的总集数 + :return: 该季不存在的集号列表 + """ + if not self.server: + return None + return self.server.get_no_exists_episodes(meta_info, + season_number, + episode_count) + + def get_movies(self, title, year=None): + """ + 根据标题和年份,检查电影是否在媒体服务器中存在,存在则返回列表 + :param title: 标题 + :param year: 年份,可以为空,为空时不按年份过滤 + :return: 含title、year属性的字典列表 + """ + if not self.server: + return None + return self.server.get_movies(title, year) + + def refresh_library_by_items(self, items): + """ + 按类型、名称、年份来刷新媒体库 + :param items: 已识别的需要刷新媒体库的媒体信息列表 + """ + if not self.server: + return + return self.server.refresh_library_by_items(items) + + def get_libraries(self): + """ + 获取媒体服务器所有媒体库列表 + """ + if not self.server: + return [] + return self.server.get_libraries() + + def get_items(self, parent): + """ + 获取媒体库中的所有媒体 + :param parent: 上一级的ID + """ + if not self.server: + return [] + return self.server.get_items(parent) + + def sync_mediaserver(self): + """ + 同步媒体库所有数据到本地数据库 + """ + if not self.server: + return + with lock: + # 开始进度条 + log.info("【MediaServer】开始同步媒体库数据...") + self.progress.start("mediasync") + self.progress.update(ptype="mediasync", text="请稍候...") + # 汇总统计 + medias_count = self.get_medias_count() + total_media_count = medias_count.get("MovieCount") + medias_count.get("SeriesCount") + total_count = 0 + movie_count = 0 + tv_count = 0 + # 清空登记薄 + self.mediadb.empty() + for library in self.get_libraries(): + # 获取媒体库所有项目 + self.progress.update(ptype="mediasync", + text="正在获取 %s 数据..." % (library.get("name"))) + for item in self.get_items(library.get("id")): + if not item: + continue + if self.mediadb.insert(self._server_type.value, item): + total_count += 1 + if item.get("type") in ['Movie', 'movie']: + movie_count += 1 + elif item.get("type") in ['Series', 'show']: + tv_count += 1 + self.progress.update(ptype="mediasync", + text="正在同步 %s,已完成:%s / %s ..." % ( + library.get("name"), total_count, total_media_count), + value=round(100 * total_count / total_media_count, 1)) + # 更新总体同步情况 + self.mediadb.statistics(server_type=self._server_type.value, + total_count=total_count, + movie_count=movie_count, + tv_count=tv_count) + # 结束进度条 + self.progress.update(ptype="mediasync", + value=100, + text="媒体库数据同步完成,同步数量:%s" % total_count) + self.progress.end("mediasync") + log.info("【MediaServer】媒体库数据同步完成,同步数量:%s" % total_count) + + def check_item_exists(self, title, year=None, tmdbid=None): + """ + 检查媒体库是否已存在某项目,非实时同步数据,仅用于展示 + """ + return self.mediadb.exists(server_type=self._server_type.value, + title=title, + year=year, + tmdbid=tmdbid) + + def get_mediasync_status(self): + """ + 获取当前媒体库同步状态 + """ + status = self.mediadb.get_statistics(server_type=self._server_type.value) + if not status: + return {} + else: + return {"movie_count": status.MOVIE_COUNT, "tv_count": status.TV_COUNT, "time": status.UPDATE_TIME} + + def get_iteminfo(self, itemid): + """ + 根据ItemId从媒体服务器查询项目详情 + :param itemid: 在Emby中的ID + :return: 图片对应在TMDB中的URL + """ + if not self.server: + return None + return self.server.get_iteminfo(itemid) + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + if not self.server: + return None + return self.server.get_playing_sessions() diff --git a/app/mediaserver/webhook_event.py b/app/mediaserver/webhook_event.py new file mode 100644 index 0000000..2953b0b --- /dev/null +++ b/app/mediaserver/webhook_event.py @@ -0,0 +1,198 @@ +import time + +from app.message import Message +from app.mediaserver import MediaServer +from app.media import Media +from web.backend.web_utils import WebUtils + + +class WebhookEvent: + message = None + mediaserver = None + media = None + + def __init__(self): + self.message = Message() + self.mediaserver = MediaServer() + self.media = Media() + + @staticmethod + def __parse_plex_msg(message): + """ + 解析Plex报文 + """ + eventItem = {'event': message.get('event', {}), + 'item_name': message.get('Metadata', {}).get('title'), + 'user_name': message.get('Account', {}).get('title') + } + return eventItem + + @staticmethod + def __parse_jellyfin_msg(message): + """ + 解析Jellyfin报文 + """ + eventItem = {'event': message.get('NotificationType', {}), + 'item_name': message.get('Name'), + 'user_name': message.get('NotificationUsername') + } + return eventItem + + @staticmethod + def __parse_emby_msg(message): + """ + 解析Emby报文 + """ + eventItem = {'event': message.get('Event', {})} + if message.get('Item'): + if message.get('Item', {}).get('Type') == 'Episode': + eventItem['item_type'] = "TV" + eventItem['item_name'] = "%s %s%s %s" % ( + message.get('Item', {}).get('SeriesName'), + "S" + str(message.get('Item', {}).get('ParentIndexNumber')), + "E" + str(message.get('Item', {}).get('IndexNumber')), + message.get('Item', {}).get('Name')) + eventItem['item_id'] = message.get('Item', {}).get('SeriesId') + eventItem['season_id'] = message.get('Item', {}).get('ParentIndexNumber') + eventItem['episode_id'] = message.get('Item', {}).get('IndexNumber') + eventItem['tmdb_id'] = message.get('Item', {}).get('ProviderIds', {}).get('Tmdb') + if message.get('Item', {}).get('Overview') and len(message.get('Item', {}).get('Overview')) > 100: + eventItem['overview'] = str(message.get('Item', {}).get('Overview'))[:100] + "..." + else: + eventItem['overview'] = message.get('Item', {}).get('Overview') + eventItem['percentage'] = message.get('TranscodingInfo', {}).get('CompletionPercentage') + else: + eventItem['item_type'] = "MOV" + eventItem['item_name'] = "%s %s" % ( + message.get('Item', {}).get('Name'), "(" + str(message.get('Item', {}).get('ProductionYear')) + ")") + eventItem['item_path'] = message.get('Item', {}).get('Path') + eventItem['item_id'] = message.get('Item', {}).get('Id') + eventItem['tmdb_id'] = message.get('Item', {}).get('ProviderIds', {}).get('Tmdb') + if len(message.get('Item', {}).get('Overview')) > 100: + eventItem['overview'] = str(message.get('Item', {}).get('Overview'))[:100] + "..." + else: + eventItem['overview'] = message.get('Item', {}).get('Overview') + eventItem['percentage'] = message.get('TranscodingInfo', {}).get('CompletionPercentage') + if message.get('Session'): + eventItem['ip'] = message.get('Session').get('RemoteEndPoint') + eventItem['device_name'] = message.get('Session').get('DeviceName') + eventItem['client'] = message.get('Session').get('Client') + if message.get("User"): + eventItem['user_name'] = message.get("User").get('Name') + + return eventItem + + def plex_action(self, message): + """ + 执行Plex webhook动作 + """ + event_info = self.__parse_plex_msg(message) + if event_info.get("event") in ["media.play", "media.stop"]: + self.send_webhook_message(event_info, 'plex') + + def jellyfin_action(self, message): + """ + 执行Jellyfin webhook动作 + """ + event_info = self.__parse_jellyfin_msg(message) + if event_info.get("event") in ["PlaybackStart", "PlaybackStop"]: + self.send_webhook_message(event_info, 'jellyfin') + + def emby_action(self, message): + """ + 执行Emby webhook动作 + """ + event_info = self.__parse_emby_msg(message) + if event_info.get("event") == "system.webhooktest": + return + elif event_info.get("event") in ["playback.start", + "playback.stop", + "user.authenticated", + "user.authenticationfailed"]: + self.send_webhook_message(event_info, 'emby') + + def send_webhook_message(self, event_info, channel): + """ + 发送消息 + """ + _webhook_actions = { + "system.webhooktest": "测试", + "playback.start": "开始播放", + "playback.stop": "停止播放", + "playback.pause": "暂停播放", + "playback.unpause": "开始播放", + "user.authenticated": "登录成功", + "user.authenticationfailed": "登录失败", + "media.play": "开始播放", + "PlaybackStart": "开始播放", + "PlaybackStop": "停止播放", + "media.stop": "停止播放", + "item.rate": "标记了", + } + _webhook_images = { + "emby": "https://emby.media/notificationicon.png", + "plex": "https://www.plex.tv/wp-content/uploads/2022/04/new-logo-process-lines-gray.png", + "jellyfin": "https://play-lh.googleusercontent.com/SCsUK3hCCRqkJbmLDctNYCfehLxsS4ggD1ZPHIFrrAN1Tn9yhjmGMPep2D9lMaaa9eQi" + } + + if self.is_ignore_webhook_message(event_info.get('user_name'), event_info.get('device_name')): + return + + # 消息标题 + if event_info.get('item_type') == "TV": + message_title = f"{_webhook_actions.get(event_info.get('event'))}剧集 {event_info.get('item_name')}" + elif event_info.get('item_type') == "MOV": + message_title = f"{_webhook_actions.get(event_info.get('event'))}电影 {event_info.get('item_name')}" + else: + message_title = f"{_webhook_actions.get(event_info.get('event'))}" + + # 消息内容 + if {event_info.get('user_name')}: + message_texts = [f"用户:{event_info.get('user_name')}"] + if event_info.get('device_name'): + message_texts.append(f"设备:{event_info.get('client')} {event_info.get('device_name')}") + if event_info.get('ip'): + message_texts.append(f"位置:{event_info.get('ip')} {WebUtils.get_location(event_info.get('ip'))}") + if event_info.get('percentage'): + percentage = round(float(event_info.get('percentage')), 2) + message_texts.append(f"进度:{percentage}%") + if event_info.get('overview'): + message_texts.append(f"剧情:{event_info.get('overview')}") + message_texts.append(f"时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") + + # 消息图片 + image_url = '' + if event_info.get('item_id'): + if event_info.get("item_type") == "TV": + iteminfo = self.mediaserver.get_iteminfo(event_info.get('item_id')) + tmdb_id = iteminfo.get('ProviderIds', {}).get('Tmdb') + try: + # 从tmdb获取剧集某季某集图片 + image_url = self.media.get_episode_images(tmdb_id, + event_info.get('season_id'), + event_info.get('episode_id')) + except IOError: + pass + + if not image_url: + image_url = self.mediaserver.get_image_by_id(event_info.get('item_id'), + "Backdrop") or _webhook_images.get(channel) + else: + image_url = _webhook_images.get(channel) + # 发送消息 + self.message.send_mediaserver_message(title=message_title, text="\n".join(message_texts), image=image_url) + + def is_ignore_webhook_message(self, user_name, device_name): + """ + 判断是否忽略通知 + """ + if not user_name and not device_name: + return False + webhook_ignore = self.message.get_webhook_ignore() + if not webhook_ignore: + return False + if user_name in webhook_ignore or \ + device_name in webhook_ignore or \ + (user_name + ':' + device_name) in webhook_ignore: + return True + return False diff --git a/app/message/__init__.py b/app/message/__init__.py new file mode 100644 index 0000000..4c0632e --- /dev/null +++ b/app/message/__init__.py @@ -0,0 +1,2 @@ +from .message import Message +from .message_center import MessageCenter diff --git a/app/message/client/__init__.py b/app/message/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/message/client/_base.py b/app/message/client/_base.py new file mode 100644 index 0000000..872347f --- /dev/null +++ b/app/message/client/_base.py @@ -0,0 +1,35 @@ +from abc import ABCMeta, abstractmethod + + +class _IMessageClient(metaclass=ABCMeta): + + @abstractmethod + def match(self, ctype): + """ + 匹配实例 + """ + pass + + @abstractmethod + def send_msg(self, title, text, image, url, user_id): + """ + 消息发送入口,支持文本、图片、链接跳转、指定发送对象 + :param title: 消息标题 + :param text: 消息内容 + :param image: 图片地址 + :param url: 点击消息跳转URL + :param user_id: 消息发送对象的ID,为空则发给所有人 + :return: 发送状态,错误信息 + """ + pass + + @abstractmethod + def send_list_msg(self, medias: list, user_id="", title="", url=""): + """ + 发送列表类消息 + :param title: 消息标题 + :param medias: 媒体列表 + :param user_id: 消息发送对象的ID,为空则发给所有人 + :param url: 跳转链接地址 + """ + pass diff --git a/app/message/client/bark.py b/app/message/client/bark.py new file mode 100644 index 0000000..252c1a6 --- /dev/null +++ b/app/message/client/bark.py @@ -0,0 +1,63 @@ +from urllib.parse import quote_plus + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, StringUtils, ExceptionUtils + + +class Bark(_IMessageClient): + schema = "bark" + + _server = None + _apikey = None + _params = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._server = StringUtils.get_base_url(self._client_config.get('server')) + self._apikey = self._client_config.get('apikey') + self._params = self._client_config.get('params') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Bark消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + :return: 发送状态、错误信息 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + try: + if not self._server or not self._apikey: + return False, "参数未配置" + sc_url = "%s/%s/%s/%s" % (self._server, self._apikey, quote_plus(title), quote_plus(text)) + if self._params: + sc_url = "%s?%s" % (sc_url, self._params) + res = RequestUtils().post_res(sc_url) + if res: + ret_json = res.json() + code = ret_json['code'] + message = ret_json['message'] + if code == 200: + return True, message + else: + return False, message + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/chanify.py b/app/message/client/chanify.py new file mode 100644 index 0000000..41cc193 --- /dev/null +++ b/app/message/client/chanify.py @@ -0,0 +1,61 @@ +from urllib import parse + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, StringUtils, ExceptionUtils + + +class Chanify(_IMessageClient): + schema = "chanify" + + _server = None + _token = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._server = StringUtils.get_base_url(self._client_config.get('server')) + self._token = self._client_config.get('token') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Bark消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + :return: 发送状态、错误信息 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + try: + if not self._server or not self._token: + return False, "参数未配置" + sc_url = "%s/v1/sender/%s" % (self._server, self._token) + # 发送文本 + data = parse.urlencode({ + 'title': title, + 'text': text + }).encode() + res = RequestUtils().post_res(sc_url, params=data) + if res: + if res.status_code == 200: + return True, "发送成功" + else: + return False, "错误码:%s" % res.status_code + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/gotify.py b/app/message/client/gotify.py new file mode 100644 index 0000000..11b0cab --- /dev/null +++ b/app/message/client/gotify.py @@ -0,0 +1,71 @@ +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, StringUtils, ExceptionUtils + + +class Gotify(_IMessageClient): + schema = "gotify" + + _server = None + _token = None + _priority = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._server = StringUtils.get_base_url(self._client_config.get('server')) + self._token = self._client_config.get('token') + try: + self._priority = int(self._client_config.get('priority')) + except Exception as e: + self._priority = 8 + ExceptionUtils.exception_traceback(e) + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Bark消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 点击消息跳转URL, 为空时则没有任何动作 + :param user_id: 未使用 + :return: 发送状态、错误信息 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + try: + if not self._server or not self._token: + return False, "参数未配置" + sc_url = "%s/message?token=%s" % (self._server, self._token) + sc_data = { + "title": title, + "message": text, + "priority": self._priority, + "extras": { + "client::notification": { + "click": { + "url": url + } + }, + } + } + res = RequestUtils(content_type="application/json").post_res(sc_url, json=sc_data) + if res and res.status_code == 200: + return True, "发送成功" + elif res: + return False, f"错误码:{res.status_code}" + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/iyuu.py b/app/message/client/iyuu.py new file mode 100644 index 0000000..4787d62 --- /dev/null +++ b/app/message/client/iyuu.py @@ -0,0 +1,56 @@ +from urllib.parse import urlencode + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, ExceptionUtils + + +class IyuuMsg(_IMessageClient): + schema = "iyuu" + + _token = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._token = self._client_config.get('token') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送爱语飞飞消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not self._token: + return False, "参数未配置" + try: + sc_url = "http://iyuu.cn/%s.send?%s" % (self._token, urlencode({"text": title, "desp": text})) + res = RequestUtils().get_res(sc_url) + if res: + ret_json = res.json() + errno = ret_json.get('errcode') + error = ret_json.get('errmsg') + if errno == 0: + return True, error + else: + return False, error + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/pushdeer.py b/app/message/client/pushdeer.py new file mode 100644 index 0000000..aa3b018 --- /dev/null +++ b/app/message/client/pushdeer.py @@ -0,0 +1,53 @@ +from pypushdeer import PushDeer + +from app.message.client._base import _IMessageClient +from app.utils import StringUtils, ExceptionUtils + + +class PushDeerClient(_IMessageClient): + schema = "pushdeer" + + _server = None + _apikey = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._server = StringUtils.get_base_url(self._client_config.get('server')) + self._apikey = self._client_config.get('apikey') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送PushDeer消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + :return: 发送状态、错误信息 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + try: + if not self._server or not self._apikey: + return False, "参数未配置" + pushdeer = PushDeer(server=self._server, pushkey=self._apikey) + res = pushdeer.send_markdown(title, desp=text) + if res: + return True, "成功" + else: + return False, "失败" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/pushplus.py b/app/message/client/pushplus.py new file mode 100644 index 0000000..d2dbdff --- /dev/null +++ b/app/message/client/pushplus.py @@ -0,0 +1,74 @@ +import time +from urllib.parse import urlencode + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, ExceptionUtils + + +class PushPlus(_IMessageClient): + schema = "pushplus" + + _token = None + _topic = None + _channel = None + _webhook = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._token = self._client_config.get('token') + self._topic = self._client_config.get('topic') + self._channel = self._client_config.get('channel') + self._webhook = self._client_config.get('webhook') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送ServerChan消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not text: + text = "无" + if not self._token or not self._channel: + return False, "参数未配置" + try: + values = { + "token": self._token, + "channel": self._channel, + "topic": self._topic, + "webhook": self._webhook, + "title": title, + "content": text, + "timestamp": time.time_ns() + 60 + } + sc_url = "http://www.pushplus.plus/send?%s" % urlencode(values) + res = RequestUtils().get_res(sc_url) + if res: + ret_json = res.json() + code = ret_json.get("code") + msg = ret_json.get("msg") + if code == 200: + return True, msg + else: + return False, msg + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/serverchan.py b/app/message/client/serverchan.py new file mode 100644 index 0000000..9a770a6 --- /dev/null +++ b/app/message/client/serverchan.py @@ -0,0 +1,56 @@ +from urllib.parse import urlencode + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, ExceptionUtils + + +class ServerChan(_IMessageClient): + schema = "serverchan" + + _sckey = None + _client_config = {} + + def __init__(self, config): + self._client_config = config + self.init_config() + + def init_config(self): + if self._client_config: + self._sckey = self._client_config.get('sckey') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送ServerChan消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 未使用 + :param url: 未使用 + :param user_id: 未使用 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not self._sckey: + return False, "参数未配置" + try: + sc_url = "https://sctapi.ftqq.com/%s.send?%s" % (self._sckey, urlencode({"title": title, "desp": text})) + res = RequestUtils().get_res(sc_url) + if res: + ret_json = res.json() + errno = ret_json.get('code') + error = ret_json.get('message') + if errno == 0: + return True, error + else: + return False, error + else: + return False, "未获取到返回信息" + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, **kwargs): + pass diff --git a/app/message/client/slack.py b/app/message/client/slack.py new file mode 100644 index 0000000..fa02fdb --- /dev/null +++ b/app/message/client/slack.py @@ -0,0 +1,264 @@ +import re +from threading import Lock + +import requests +from slack_sdk.errors import SlackApiError + +import log +from app.message.client._base import _IMessageClient +from app.utils import ExceptionUtils +from config import Config +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler + +lock = Lock() + + +class Slack(_IMessageClient): + schema = "slack" + + _client_config = {} + _interactive = False + _ds_url = None + _service = None + _channel = None + _client = None + + def __init__(self, config): + self._config = Config() + self._client_config = config + self._interactive = config.get("interactive") + self._channel = config.get("channel") or "全体" + self.init_config() + + def init_config(self): + self._ds_url = "http://127.0.0.1:%s/slack" % self._config.get_config("app").get("web_port") + if self._client_config: + try: + slack_app = App(token=self._client_config.get("bot_token")) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return + self._client = slack_app.client + + # 注册消息响应 + @slack_app.event("message") + def slack_message(message): + local_res = requests.post(self._ds_url, json=message, timeout=10) + log.debug("【Slack】message: %s processed, response is: %s" % (message, local_res.text)) + + @slack_app.action(re.compile(r"actionId-\d+")) + def slack_action(ack, body): + ack() + local_res = requests.post(self._ds_url, json=body, timeout=60) + log.debug("【Slack】message: %s processed, response is: %s" % (body, local_res.text)) + + @slack_app.event("app_mention") + def slack_mention(say, body): + say(f"收到,请稍等... <@{body.get('event', {}).get('user')}>") + local_res = requests.post(self._ds_url, json=body, timeout=10) + log.debug("【Slack】message: %s processed, response is: %s" % (body, local_res.text)) + + @slack_app.shortcut(re.compile(r"/*")) + def slack_shortcut(ack, body): + ack() + local_res = requests.post(self._ds_url, json=body, timeout=10) + log.debug("【Slack】message: %s processed, response is: %s" % (body, local_res.text)) + + # 启动服务 + if self._interactive: + try: + self._service = SocketModeHandler( + slack_app, + self._client_config.get("app_token") + ) + self._service.connect() + log.info("Slack消息接收服务启动") + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("Slack消息接收服务启动失败: %s" % str(err)) + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def stop_service(self): + if self._service: + try: + self._service.close() + except Exception as err: + print(str(err)) + log.info("Slack消息接收服务已停止") + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Telegram消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 消息图片地址 + :param url: 点击消息转转的URL + :param user_id: 用户ID,如有则只发消息给该用户 + :user_id: 发送消息的目标用户ID,为空则发给管理员 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not self._client: + return False, "消息客户端未就绪" + try: + if user_id: + channel = user_id + else: + # 消息广播 + channel = self.__find_public_channel() + # 拼装消息内容 + titles = str(title).split('\n') + if len(titles) > 1: + title = titles[0] + if not text: + text = "\n".join(titles[1:]) + else: + text = "%s\n%s" % ("\n".join(titles[1:]), text) + block = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*{title}*\n{text}" + } + } + # 消息图片 + if image: + block['accessory'] = { + "type": "image", + "image_url": f"{image}", + "alt_text": f"{title}" + } + blocks = [block] + # 链接 + if image and url: + blocks.append({ + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "查看详情", + "emoji": True + }, + "value": "click_me_url", + "url": f"{url}", + "action_id": "actionId-url" + } + ] + }) + # 发送 + result = self._client.chat_postMessage( + channel=channel, + blocks=blocks + ) + return True, result + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, medias: list, user_id="", **kwargs): + """ + 发送列表类消息 + """ + if not medias: + return False, "参数有误" + if not self._client: + return False, "消息客户端未就绪" + try: + if user_id: + channel = user_id + else: + # 消息广播 + channel = self.__find_public_channel() + title = f"共找到{len(medias)}条相关信息,请选择" + # 消息主体 + title_section = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*{title}*" + } + } + blocks = [title_section] + # 列表 + if medias: + blocks.append({ + "type": "divider" + }) + index = 1 + for media in medias: + if media.get_poster_image(): + if media.get_star_string(): + text = f"{index}. *<{media.get_detail_url()}|{media.get_title_string()}>*" \ + f"\n{media.get_type_string()}" \ + f"\n{media.get_star_string()}" \ + f"\n{media.get_overview_string(50)}" + else: + text = f"{index}. *<{media.get_detail_url()}|{media.get_title_string()}>*" \ + f"\n{media.get_type_string()}" \ + f"\n{media.get_overview_string(50)}" + blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": text + }, + "accessory": { + "type": "image", + "image_url": f"{media.get_poster_image()}", + "alt_text": f"{media.get_title_string()}" + } + } + ) + blocks.append( + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "选择", + "emoji": True + }, + "value": f"{index}", + "action_id": f"actionId-{index}" + } + ] + } + ) + index += 1 + # 发送 + result = self._client.chat_postMessage( + channel=channel, + blocks=blocks + ) + return True, result + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def __find_public_channel(self): + """ + 查找公共频道 + """ + if not self._client: + return "" + conversation_id = "" + try: + for result in self._client.conversations_list(): + if conversation_id: + break + for channel in result["channels"]: + if channel.get("name") == self._channel: + conversation_id = channel.get("id") + break + except SlackApiError as e: + print(f"Slack Error: {e}") + return conversation_id diff --git a/app/message/client/synologychat.py b/app/message/client/synologychat.py new file mode 100644 index 0000000..1344b16 --- /dev/null +++ b/app/message/client/synologychat.py @@ -0,0 +1,169 @@ +import json +from urllib.parse import quote +from threading import Lock + +from app.message.client._base import _IMessageClient +from app.utils import ExceptionUtils, RequestUtils, StringUtils +from config import Config + +lock = Lock() + + +class SynologyChat(_IMessageClient): + schema = "synologychat" + + _client_config = {} + _interactive = False + _domain = None + _webhook_url = None + _token = None + _client = None + _req = None + + def __init__(self, config): + self._config = Config() + self._client_config = config + self._interactive = config.get("interactive") + self._req = RequestUtils(content_type="application/x-www-form-urlencoded") + self.init_config() + + def init_config(self): + if self._client_config: + self._webhook_url = self._client_config.get("webhook_url") + if self._webhook_url: + self._domain = StringUtils.get_base_url(self._webhook_url) + self._token = self._client_config.get('token') + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def check_token(self, token): + return True if token == self._token else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Telegram消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 消息图片地址 + :param url: 点击消息转转的URL + :param user_id: 用户ID,如有则只发消息给该用户 + :user_id: 发送消息的目标用户ID,为空则发给管理员 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not self._webhook_url or not self._token: + return False, "参数未配置" + try: + # 拼装消息内容 + titles = str(title).split('\n') + if len(titles) > 1: + title = titles[0] + if not text: + text = "\n".join(titles[1:]) + else: + text = f"%s\n%s" % ("\n".join(titles[1:]), text) + + if text: + caption = "*%s*\n%s" % (title, text.replace("\n\n", "\n")) + else: + caption = title + if url and image: + caption = f"{caption}\n\n<{url}|查看详情>" + payload_data = {'text': quote(caption)} + if image: + payload_data['file_url'] = quote(image) + if user_id: + payload_data['user_ids'] = [int(user_id)] + else: + userids = self.__get_bot_users() + if not userids: + return False, "机器人没有对任何用户可见" + payload_data['user_ids'] = userids + return self.__send_request(payload_data) + + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, medias: list, user_id="", title="", **kwargs): + """ + 发送列表类消息 + """ + if not medias: + return False, "参数有误" + if not self._webhook_url or not self._token: + return False, "参数未配置" + try: + if not title or not isinstance(medias, list): + return False, "数据错误" + index, image, caption = 1, "", "*%s*" % title + for media in medias: + if not image: + image = media.get_message_image() + if media.get_vote_string(): + caption = "%s\n%s. <%s|%s>\n%s,%s" % (caption, + index, + media.get_detail_url(), + media.get_title_string(), + media.get_type_string(), + media.get_vote_string()) + else: + caption = "%s\n%s. <%s|%s>\n%s" % (caption, + index, + media.get_detail_url(), + media.get_title_string(), + media.get_type_string()) + index += 1 + + if user_id: + user_ids = [int(user_id)] + else: + user_ids = self.__get_bot_users() + payload_data = { + "text": quote(caption), + "file_url": quote(image), + "user_ids": user_ids + } + return self.__send_request(payload_data) + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def __get_bot_users(self): + """ + 查询机器人可见的用户列表 + """ + if not self._domain or not self._token: + return [] + req_url = f"{self._domain}" \ + f"/webapi/entry.cgi?api=SYNO.Chat.External&method=user_list&version=2&token=" \ + f"{self._token}" + ret = self._req.get_res(url=req_url) + if ret and ret.status_code == 200: + users = ret.json().get("data", {}).get("users", []) or [] + return [user.get("user_id") for user in users] + else: + return [] + + def __send_request(self, payload_data): + """ + 发送消息请求 + """ + payload = f"payload={json.dumps(payload_data)}" + ret = self._req.post_res(url=self._webhook_url, params=payload) + if ret and ret.status_code == 200: + result = ret.json() + if result: + errno = result.get('error', {}).get('code') + errmsg = result.get('error', {}).get('errors') + if not errno: + return True, "" + return False, f"{errno}-{errmsg}" + else: + return False, f"{ret.text}" + elif ret: + return False, f"错误码:{ret.status_code}" + else: + return False, "未获取到返回信息" diff --git a/app/message/client/telegram.py b/app/message/client/telegram.py new file mode 100644 index 0000000..67a3a49 --- /dev/null +++ b/app/message/client/telegram.py @@ -0,0 +1,317 @@ +from threading import Event, Lock +from urllib.parse import urlencode + +import requests + +import log +from app.helper import ThreadHelper +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, ExceptionUtils +from config import Config + +lock = Lock() +WEBHOOK_STATUS = False + + +class Telegram(_IMessageClient): + schema = "telegram" + + _telegram_token = None + _telegram_chat_id = None + _webhook = None + _webhook_url = None + _telegram_user_ids = [] + _telegram_admin_ids = [] + _domain = None + _message_proxy_event = None + _client_config = {} + _interactive = False + _enabled = True + + def __init__(self, config): + self._client_config = config + self._interactive = config.get("interactive") + self._domain = Config().get_domain() + if self._domain and self._domain.endswith("/"): + self._domain = self._domain[:-1] + self.init_config() + + def init_config(self): + if self._client_config: + self._telegram_token = self._client_config.get('token') + self._telegram_chat_id = self._client_config.get('chat_id') + self._webhook = self._client_config.get('webhook') + telegram_admin_ids = self._client_config.get('admin_ids') + if telegram_admin_ids: + self._telegram_admin_ids = telegram_admin_ids.split(",") + self._telegram_user_ids = self._telegram_admin_ids + telegram_user_ids = self._client_config.get('user_ids') + if telegram_user_ids: + self._telegram_user_ids.extend(telegram_user_ids.split(",")) + if self._telegram_token and self._telegram_chat_id: + if self._webhook: + if self._domain: + self._webhook_url = "%s/telegram" % self._domain + self.__set_bot_webhook() + if self._message_proxy_event: + self._message_proxy_event.set() + self._message_proxy_event = None + elif self._interactive: + self.__del_bot_webhook() + if not self._message_proxy_event: + event = Event() + self._message_proxy_event = event + ThreadHelper().start_thread(self.__start_telegram_message_proxy, [event]) + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def get_admin(self): + """ + 获取允许使用远程命令的user_id列表 + """ + return self._telegram_admin_ids + + def get_users(self): + """ + 获取允许使用telegram机器人的user_id列表 + """ + return self._telegram_user_ids + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送Telegram消息 + :param title: 消息标题 + :param text: 消息内容 + :param image: 消息图片地址 + :param url: 点击消息转转的URL + :param user_id: 用户ID,如有则只发消息给该用户 + :user_id: 发送消息的目标用户ID,为空则发给管理员 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + try: + if not self._telegram_token or not self._telegram_chat_id: + return False, "参数未配置" + + # text中的Markdown特殊字符转义 + text = text.replace("[", r"\[").replace("_", r"\_").replace("*", r"\*").replace("`", r"\`") + # 拼装消息内容 + titles = str(title).split('\n') + if len(titles) > 1: + title = titles[0] + if not text: + text = "\n".join(titles[1:]) + else: + text = "%s\n%s" % ("\n".join(titles[1:]), text) + if text: + caption = "*%s*\n%s" % (title, text.replace("\n\n", "\n")) + else: + caption = title + if image and url: + caption = "%s\n\n[查看详情](%s)" % (caption, url) + if user_id: + chat_id = user_id + else: + chat_id = self._telegram_chat_id + + return self.__send_request(chat_id=chat_id, image=image, caption=caption) + + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, medias: list, user_id="", title="", **kwargs): + """ + 发送列表类消息 + """ + try: + if not self._telegram_token or not self._telegram_chat_id: + return False, "参数未配置" + if not title or not isinstance(medias, list): + return False, "数据错误" + index, image, caption = 1, "", "*%s*" % title + for media in medias: + if not image: + image = media.get_message_image() + if media.get_vote_string(): + caption = "%s\n%s. [%s](%s)\n%s,%s" % (caption, + index, + media.get_title_string(), + media.get_detail_url(), + media.get_type_string(), + media.get_vote_string()) + else: + caption = "%s\n%s. [%s](%s)\n%s" % (caption, + index, + media.get_title_string(), + media.get_detail_url(), + media.get_type_string()) + index += 1 + + if user_id: + chat_id = user_id + else: + chat_id = self._telegram_chat_id + + return self.__send_request(chat_id=chat_id, image=image, caption=caption) + + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def __send_request(self, chat_id="", image="", caption=""): + """ + 向Telegram发送报文 + """ + def _res_parse(result): + if result: + ret_json = result.json() + status = ret_json.get("ok") + if status: + return True, "" + else: + return False, ret_json.get("description") + else: + return False, "未获取到返回信息" + + proxies = Config().get_proxies() + if image: + # 发送图文消息 + values = {"chat_id": chat_id, "photo": image, "caption": caption, "parse_mode": "Markdown"} + sc_url = "https://api.telegram.org/bot%s/sendPhoto?" % self._telegram_token + res = RequestUtils(proxies=proxies).get_res(sc_url + urlencode(values)) + flag, msg = _res_parse(res) + if flag: + return flag, msg + else: + photo_req = RequestUtils(proxies=proxies).get_res(image) + if photo_req and photo_req.content: + sc_url = "https://api.telegram.org/bot%s/sendPhoto" % self._telegram_token + data = {"chat_id": chat_id, "caption": caption, "parse_mode": "Markdown"} + files = {"photo": photo_req.content} + res = requests.post(sc_url, proxies=proxies, data=data, files=files) + flag, msg = _res_parse(res) + if flag: + return flag, msg + # 发送文本消息 + values = {"chat_id": chat_id, "text": caption, "parse_mode": "Markdown"} + sc_url = "https://api.telegram.org/bot%s/sendMessage?" % self._telegram_token + res = RequestUtils(proxies=proxies).get_res(sc_url + urlencode(values)) + flag, msg = _res_parse(res) + return flag, msg + + def __set_bot_webhook(self): + """ + 设置Telegram Webhook + """ + if not self._webhook_url: + return + + try: + lock.acquire() + global WEBHOOK_STATUS + if not WEBHOOK_STATUS: + WEBHOOK_STATUS = True + else: + return + finally: + lock.release() + + status = self.__get_bot_webhook() + if status and status != 1: + if status == 2: + self.__del_bot_webhook() + values = {"url": self._webhook_url, "allowed_updates": ["message"]} + sc_url = "https://api.telegram.org/bot%s/setWebhook?" % self._telegram_token + res = RequestUtils(proxies=Config().get_proxies()).get_res(sc_url + urlencode(values)) + if res is not None: + json = res.json() + if json.get("ok"): + log.info("【Telegram】Webhook 设置成功,地址为:%s" % self._webhook_url) + else: + log.error("【Telegram】Webhook 设置失败:" % json.get("description")) + else: + log.error("【Telegram】Webhook 设置失败:网络连接故障!") + + def __get_bot_webhook(self): + """ + 获取Telegram已设置的Webhook + :return: 状态:1-存在且相等,2-存在不相等,3-不存在,0-网络出错 + """ + sc_url = "https://api.telegram.org/bot%s/getWebhookInfo" % self._telegram_token + res = RequestUtils(proxies=Config().get_proxies()).get_res(sc_url) + if res is not None and res.json(): + if res.json().get("ok"): + result = res.json().get("result") or {} + webhook_url = result.get("url") or "" + if webhook_url: + log.info("【Telegram】Webhook 地址为:%s" % webhook_url) + pending_update_count = result.get("pending_update_count") + last_error_message = result.get("last_error_message") + if pending_update_count and last_error_message: + log.warn("【Telegram】Webhook 有 %s 条消息挂起,最后一次失败原因为:%s" % ( + pending_update_count, last_error_message)) + if webhook_url == self._webhook_url: + return 1 + else: + return 2 + else: + return 3 + else: + return 0 + + def __del_bot_webhook(self): + """ + 删除Telegram Webhook + :return: 是否成功 + """ + sc_url = "https://api.telegram.org/bot%s/deleteWebhook" % self._telegram_token + res = RequestUtils(proxies=Config().get_proxies()).get_res(sc_url) + if res and res.json() and res.json().get("ok"): + return True + else: + return False + + def __start_telegram_message_proxy(self, event: Event): + log.info("Telegram消息接收服务启动") + + long_poll_timeout = 5 + + def consume_messages(_config, _offset, _sc_url, _ds_url): + try: + values = {"timeout": long_poll_timeout, "offset": _offset} + res = RequestUtils(proxies=_config.get_proxies()).get_res(_sc_url + urlencode(values)) + if res and res.json(): + for msg in res.json().get("result", []): + # 无论本地是否成功,先更新offset,即消息最多成功消费一次 + _offset = msg["update_id"] + 1 + log.debug("【Telegram】接收到消息: %s" % msg) + local_res = requests.post(_ds_url, json=msg, timeout=10) + log.debug("【Telegram】message: %s processed, response is: %s" % (msg, local_res.text)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【Telegram】消息接收出现错误: %s" % e) + return _offset + + offset = 0 + while True: + _config = Config() + web_port = _config.get_config("app").get("web_port") + sc_url = "https://api.telegram.org/bot%s/getUpdates?" % self._telegram_token + ds_url = "http://127.0.0.1:%s/telegram" % web_port + if not self._enabled: + log.info("Telegram消息接收服务已停止") + break + + i = 0 + while i < 20 and not event.is_set(): + offset = consume_messages(_config, offset, sc_url, ds_url) + i = i + 1 + + def stop_service(self): + """ + 停止服务 + """ + self._enabled = False diff --git a/app/message/client/wechat.py b/app/message/client/wechat.py new file mode 100644 index 0000000..6d78b88 --- /dev/null +++ b/app/message/client/wechat.py @@ -0,0 +1,220 @@ +import json +import threading +from datetime import datetime + +from app.message.client._base import _IMessageClient +from app.utils import RequestUtils, ExceptionUtils +from config import DEFAULT_WECHAT_PROXY + +lock = threading.Lock() + + +class WeChat(_IMessageClient): + schema = "wechat" + + _instance = None + _access_token = None + _expires_in = None + _access_token_time = None + _default_proxy = False + _client_config = {} + _corpid = None + _corpsecret = None + _agent_id = None + _interactive = False + + _send_msg_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" + _token_url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" + + def __init__(self, config): + self._client_config = config + self._interactive = config.get("interactive") + self.init_config() + + def init_config(self): + if self._client_config: + self._corpid = self._client_config.get('corpid') + self._corpsecret = self._client_config.get('corpsecret') + self._agent_id = self._client_config.get('agentid') + self._default_proxy = self._client_config.get('default_proxy') + if self._default_proxy: + if isinstance(self._default_proxy, bool): + self._send_msg_url = f"{DEFAULT_WECHAT_PROXY}/cgi-bin/message/send?access_token=%s" + self._token_url = f"{DEFAULT_WECHAT_PROXY}/cgi-bin/gettoken?corpid=%s&corpsecret=%s" + else: + self._send_msg_url = f"{self._default_proxy}/cgi-bin/message/send?access_token=%s" + self._token_url = f"{self._default_proxy}/cgi-bin/gettoken?corpid=%s&corpsecret=%s" + if self._corpid and self._corpsecret and self._agent_id: + self.__get_access_token() + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def __get_access_token(self, force=False): + """ + 获取微信Token + :return: 微信Token + """ + token_flag = True + if not self._access_token: + token_flag = False + else: + if (datetime.now() - self._access_token_time).seconds >= self._expires_in: + token_flag = False + + if not token_flag or force: + if not self._corpid or not self._corpsecret: + return None + try: + token_url = self._token_url % (self._corpid, self._corpsecret) + res = RequestUtils().get_res(token_url) + if res: + ret_json = res.json() + if ret_json.get('errcode') == 0: + self._access_token = ret_json.get('access_token') + self._expires_in = ret_json.get('expires_in') + self._access_token_time = datetime.now() + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + return self._access_token + + def __send_message(self, title, text, user_id=None): + """ + 发送文本消息 + :param title: 消息标题 + :param text: 消息内容 + :param user_id: 消息发送对象的ID,为空则发给所有人 + :return: 发送状态,错误信息 + """ + if not self.__get_access_token(): + return False, "参数未配置或配置不正确" + message_url = self._send_msg_url % self.__get_access_token() + if text: + conent = "%s\n%s" % (title, text.replace("\n\n", "\n")) + else: + conent = title + if not user_id: + user_id = "@all" + req_json = { + "touser": user_id, + "msgtype": "text", + "agentid": self._agent_id, + "text": { + "content": conent + }, + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0 + } + return self.__post_request(message_url, req_json) + + def __send_image_message(self, title, text, image_url, url, user_id=None): + """ + 发送图文消息 + :param title: 消息标题 + :param text: 消息内容 + :param image_url: 图片地址 + :param url: 点击消息跳转URL + :param user_id: 消息发送对象的ID,为空则发给所有人 + :return: 发送状态,错误信息 + """ + if not self.__get_access_token(): + return False, "获取微信access_token失败,请检查参数配置" + message_url = self._send_msg_url % self.__get_access_token() + if text: + text = text.replace("\n\n", "\n") + if not user_id: + user_id = "@all" + req_json = { + "touser": user_id, + "msgtype": "news", + "agentid": self._agent_id, + "news": { + "articles": [ + { + "title": title, + "description": text, + "picurl": image_url, + "url": url + } + ] + } + } + return self.__post_request(message_url, req_json) + + def send_msg(self, title, text="", image="", url="", user_id=None): + """ + 微信消息发送入口,支持文本、图片、链接跳转、指定发送对象 + :param title: 消息标题 + :param text: 消息内容 + :param image: 图片地址 + :param url: 点击消息跳转URL + :param user_id: 消息发送对象的ID,为空则发给所有人 + :return: 发送状态,错误信息 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if image: + ret_code, ret_msg = self.__send_image_message(title, text, image, url, user_id) + else: + ret_code, ret_msg = self.__send_message(title, text, user_id) + return ret_code, ret_msg + + def send_list_msg(self, medias: list, user_id="", title="", **kwargs): + """ + 发送列表类消息 + """ + if not self.__get_access_token(): + return False, "参数未配置或配置不正确" + if not isinstance(medias, list): + return False, "数据错误" + message_url = self._send_msg_url % self.__get_access_token() + if not user_id: + user_id = "@all" + articles = [] + index = 1 + for media in medias: + if media.get_vote_string(): + title = f"{index}. {media.get_title_string()}\n{media.get_type_string()},{media.get_vote_string()}" + else: + title = f"{index}. {media.get_title_string()}\n{media.get_type_string()}" + articles.append({ + "title": title, + "description": "", + "picurl": media.get_message_image() if index == 1 else media.get_poster_image(), + "url": media.get_detail_url() + }) + index += 1 + req_json = { + "touser": user_id, + "msgtype": "news", + "agentid": self._agent_id, + "news": { + "articles": articles + } + } + return self.__post_request(message_url, req_json) + + def __post_request(self, message_url, req_json): + """ + 向微信发送请求 + """ + headers = {'content-type': 'application/json'} + try: + res = RequestUtils(headers=headers).post(message_url, + params=json.dumps(req_json, ensure_ascii=False).encode('utf-8')) + if res: + ret_json = res.json() + if ret_json.get('errcode') == 0: + return True, ret_json.get('errmsg') + else: + if ret_json.get('errcode') == 42001: + self.__get_access_token(force=True) + return False, ret_json.get('errmsg') + else: + return False, None + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False, str(err) diff --git a/app/message/message.py b/app/message/message.py new file mode 100644 index 0000000..c553993 --- /dev/null +++ b/app/message/message.py @@ -0,0 +1,540 @@ +import json +import re +from enum import Enum + +import log +from app.conf import ModuleConf +from app.helper import DbHelper, SubmoduleHelper +from app.message.message_center import MessageCenter +from app.utils import StringUtils, ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import SearchType, MediaType +from config import Config + + +@singleton +class Message(object): + dbhelper = None + messagecenter = None + _message_schemas = [] + _active_clients = [] + _active_interactive_clients = {} + _client_configs = {} + _webhook_ignore = None + _domain = None + + def __init__(self): + self._message_schemas = SubmoduleHelper.import_submodules( + 'app.message.client', + filter_func=lambda _, obj: hasattr(obj, 'schema') + ) + log.debug(f"【Message】加载消息服务:{self._message_schemas}") + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.messagecenter = MessageCenter() + self._domain = Config().get_domain() + # 停止旧服务 + if self._active_clients: + for active_client in self._active_clients: + if active_client.get("search_type") in self.get_search_types(): + client = active_client.get("client") + if client and hasattr(client, "stop_service"): + client.stop_service() + # 活跃的客户端 + self._active_clients = [] + # 活跃的交互客户端 + self._active_interactive_clients = {} + # 全量客户端配置 + self._client_configs = {} + for client_config in self.dbhelper.get_message_client() or []: + config = json.loads(client_config.CONFIG) if client_config.CONFIG else {} + config.update({ + "interactive": client_config.INTERACTIVE + }) + client_conf = { + "id": client_config.ID, + "name": client_config.NAME, + "type": client_config.TYPE, + "config": config, + "switchs": json.loads(client_config.SWITCHS) if client_config.SWITCHS else [], + "interactive": client_config.INTERACTIVE, + "enabled": client_config.ENABLED + } + self._client_configs[str(client_config.ID)] = client_conf + if not client_config.ENABLED or not config: + continue + client = { + "search_type": ModuleConf.MESSAGE_CONF.get('client').get(client_config.TYPE, {}).get('search_type'), + "client": self.__build_class(ctype=client_config.TYPE, conf=config) + } + client.update(client_conf) + self._active_clients.append(client) + if client.get("interactive"): + self._active_interactive_clients[client.get("search_type")] = client + + def __build_class(self, ctype, conf): + for message_schema in self._message_schemas: + try: + if message_schema.match(ctype): + return message_schema(conf) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + def get_status(self, ctype=None, config=None): + """ + 测试消息设置状态 + """ + if not config or not ctype: + return False + # 测试状态不启动监听服务 + state, ret_msg = self.__build_class(ctype=ctype, + conf=config).send_msg(title="测试", + text="这是一条测试消息", + url="https://github.com/NAStool/nas-tools") + if not state: + log.error(f"【Message】{ctype} 发送测试消息失败:%s" % ret_msg) + return state + + def get_webhook_ignore(self): + """ + 获取Emby/Jellyfin不通知的设备清单 + """ + return self._webhook_ignore or [] + + def __sendmsg(self, client, title, text="", image="", url="", user_id=""): + """ + 通用消息发送 + :param client: 消息端 + :param title: 消息标题 + :param text: 消息内容 + :param image: 图片URL + :param url: 消息跳转地址 + :param user_id: 用户ID,如有则只发给这个用户 + :return: 发送状态、错误信息 + """ + if not client or not client.get('client'): + return None + cname = client.get('name') + log.info(f"【Message】发送消息 {cname}:title={title}, text={text}") + if self._domain: + if url: + if not url.startswith("http"): + url = "%s?next=%s" % (self._domain, url) + else: + url = self._domain + else: + url = "" + state, ret_msg = client.get('client').send_msg(title=title, + text=text, + image=image, + url=url, + user_id=user_id) + if not state: + log.error(f"【Message】{cname} 消息发送失败:%s" % ret_msg) + return state + + def send_channel_msg(self, channel, title, text="", image="", url="", user_id=""): + """ + 按渠道发送消息,用于消息交互 + :param channel: 消息渠道 + :param title: 消息标题 + :param text: 消息内容 + :param image: 图片URL + :param url: 消息跳转地址 + :param user_id: 用户ID,如有则只发给这个用户 + :return: 发送状态、错误信息 + """ + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + client = self._active_interactive_clients.get(channel) + if client: + state = self.__sendmsg(client=client, + title=title, + text=text, + image=image, + url=url, + user_id=user_id) + return state + return False + + def __send_list_msg(self, client, medias, user_id, title): + """ + 发送选择类消息 + """ + if not client or not client.get('client'): + return None + cname = client.get('name') + log.info(f"【Message】发送消息 {cname}:title={title}") + state, ret_msg = client.get('client').send_list_msg(medias=medias, + user_id=user_id, + title=title, + url=self._domain) + if not state: + log.error(f"【Message】{cname} 发送消息失败:%s" % ret_msg) + return state + + def send_channel_list_msg(self, channel, title, medias: list, user_id=""): + """ + 发送列表选择消息,用于消息交互 + :param channel: 消息渠道 + :param title: 消息标题 + :param medias: 媒体信息列表 + :param user_id: 用户ID,如有则只发给这个用户 + :return: 发送状态、错误信息 + """ + client = self._active_interactive_clients.get(channel) + if client: + state = self.__send_list_msg(client=client, + title=title, + medias=medias, + user_id=user_id) + return state + return False + + def send_download_message(self, in_from: SearchType, can_item): + """ + 发送下载的消息 + :param in_from: 下载来源 + :param can_item: 下载的媒体信息 + :return: 发送状态、错误信息 + """ + msg_title = f"{can_item.get_title_ep_string()} 开始下载" + msg_text = f"{can_item.get_star_string()}" + msg_text = f"{msg_text}\n来自:{in_from.value}" + if can_item.user_name: + msg_text = f"{msg_text}\n用户:{can_item.user_name}" + if can_item.site: + if in_from == SearchType.USERRSS: + msg_text = f"{msg_text}\n任务:{can_item.site}" + else: + msg_text = f"{msg_text}\n站点:{can_item.site}" + if can_item.get_resource_type_string(): + msg_text = f"{msg_text}\n质量:{can_item.get_resource_type_string()}" + if can_item.size: + if str(can_item.size).isdigit(): + size = StringUtils.str_filesize(can_item.size) + else: + size = can_item.size + msg_text = f"{msg_text}\n大小:{size}" + if can_item.org_string: + msg_text = f"{msg_text}\n种子:{can_item.org_string}" + if can_item.seeders: + msg_text = f"{msg_text}\n做种数:{can_item.seeders}" + msg_text = f"{msg_text}\n促销:{can_item.get_volume_factor_string()}" + if can_item.hit_and_run: + msg_text = f"{msg_text}\nHit&Run:是" + if can_item.description: + html_re = re.compile(r'<[^>]+>', re.S) + description = html_re.sub('', can_item.description) + can_item.description = re.sub(r'<[^>]+>', '', description) + msg_text = f"{msg_text}\n描述:{can_item.description}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=msg_title, content=msg_text) + # 发送消息 + for client in self._active_clients: + if "download_start" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_text, + image=can_item.get_message_image(), + url='downloading' + ) + + def send_transfer_movie_message(self, in_from: Enum, media_info, exist_filenum, category_flag): + """ + 发送转移电影的消息 + :param in_from: 转移来源 + :param media_info: 转移的媒体信息 + :param exist_filenum: 已存在的文件数 + :param category_flag: 二级分类开关 + :return: 发送状态、错误信息 + """ + msg_title = f"{media_info.get_title_string()} 已入库" + if media_info.vote_average: + msg_str = f"{media_info.get_vote_string()},类型:电影" + else: + msg_str = "类型:电影" + if media_info.category: + if category_flag: + msg_str = f"{msg_str},类别:{media_info.category}" + if media_info.get_resource_type_string(): + msg_str = f"{msg_str},质量:{media_info.get_resource_type_string()}" + msg_str = f"{msg_str},大小:{StringUtils.str_filesize(media_info.size)},来自:{in_from.value}" + if exist_filenum != 0: + msg_str = f"{msg_str},{exist_filenum}个文件已存在" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "transfer_finished" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=media_info.get_message_image(), + url='history' + ) + + def send_transfer_tv_message(self, message_medias: dict, in_from: Enum): + """ + 发送转移电视剧/动漫的消息 + """ + for item_info in message_medias.values(): + if item_info.total_episodes == 1: + msg_title = f"{item_info.get_title_string()} {item_info.get_season_episode_string()} 已入库" + else: + msg_title = f"{item_info.get_title_string()} {item_info.get_season_string()} 共{item_info.total_episodes}集 已入库" + if item_info.vote_average: + msg_str = f"{item_info.get_vote_string()},类型:{item_info.type.value}" + else: + msg_str = f"类型:{item_info.type.value}" + if item_info.category: + msg_str = f"{msg_str},类别:{item_info.category}" + if item_info.total_episodes == 1: + msg_str = f"{msg_str},大小:{StringUtils.str_filesize(item_info.size)},来自:{in_from.value}" + else: + msg_str = f"{msg_str},总大小:{StringUtils.str_filesize(item_info.size)},来自:{in_from.value}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "transfer_finished" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=item_info.get_message_image(), + url='history') + + def send_download_fail_message(self, item, error_msg): + """ + 发送下载失败的消息 + """ + title = "添加下载任务失败:%s %s" % (item.get_title_string(), item.get_season_episode_string()) + text = f"站点:{item.site}\n种子名称:{item.org_string}\n种子链接:{item.enclosure}\n错误信息:{error_msg}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "download_fail" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + image=item.get_message_image() + ) + + def send_rss_success_message(self, in_from: Enum, media_info): + """ + 发送订阅成功的消息 + """ + if media_info.type == MediaType.MOVIE: + msg_title = f"{media_info.get_title_string()} 已添加订阅" + else: + msg_title = f"{media_info.get_title_string()} {media_info.get_season_string()} 已添加订阅" + msg_str = f"类型:{media_info.type.value}" + if media_info.vote_average: + msg_str = f"{msg_str},{media_info.get_vote_string()}" + msg_str = f"{msg_str},来自:{in_from.value}" + if media_info.user_name: + msg_str = f"{msg_str},用户:{media_info.user_name}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "rss_added" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=media_info.get_message_image(), + url='movie_rss' if media_info.type == MediaType.MOVIE else 'tv_rss' + ) + + def send_rss_finished_message(self, media_info): + """ + 发送订阅完成的消息,只针对电视剧 + """ + if media_info.type == MediaType.MOVIE: + return + else: + if media_info.over_edition: + msg_title = f"{media_info.get_title_string()} {media_info.get_season_string()} 已完成洗版" + else: + msg_title = f"{media_info.get_title_string()} {media_info.get_season_string()} 已完成订阅" + msg_str = f"类型:{media_info.type.value}" + if media_info.vote_average: + msg_str = f"{msg_str},{media_info.get_vote_string()}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "rss_finished" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=media_info.get_message_image(), + url='downloaded' + ) + + def send_site_signin_message(self, msgs: list): + """ + 发送站点签到消息 + """ + if not msgs: + return + title = "站点签到" + text = "\n".join(msgs) + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "site_signin" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text + ) + + def send_site_message(self, title=None, text=None): + """ + 发送站点消息 + """ + if not title: + return + if not text: + text = "" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "site_message" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text + ) + + def send_transfer_fail_message(self, path, count, text): + """ + 发送转移失败的消息 + """ + if not path or not count: + return + title = f"【{count} 个文件入库失败】" + text = f"源路径:{path}\n原因:{text}" + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "transfer_fail" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + url="unidentification" + ) + + def send_brushtask_remove_message(self, title, text): + """ + 发送刷流删种的消息 + """ + if not title or not text: + return + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "brushtask_remove" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + url="brushtask" + ) + + def send_brushtask_added_message(self, title, text): + """ + 发送刷流下种的消息 + """ + if not title or not text: + return + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "brushtask_added" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + url="brushtask" + ) + + def send_mediaserver_message(self, title, text, image): + """ + 发送媒体服务器的消息 + """ + if not title or not text or not image: + return + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "mediaserver_message" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + image=image + ) + + def send_custom_message(self, title, text="", image=""): + """ + 发送自定义消息 + """ + if not title: + return + # 插入消息中心 + self.messagecenter.insert_system_message(level="INFO", title=title, content=text) + # 发送消息 + for client in self._active_clients: + if "custom_message" in client.get("switchs"): + self.__sendmsg( + client=client, + title=title, + text=text, + image=image + ) + + def get_message_client_info(self, cid=None): + """ + 获取消息端信息 + """ + if cid: + return self._client_configs.get(str(cid)) + return self._client_configs + + def get_interactive_client(self, client_type=None): + """ + 查询当前可以交互的渠道 + """ + if client_type: + return self._active_interactive_clients.get(client_type) + else: + return [client for client in self._active_interactive_clients.values()] + + @staticmethod + def get_search_types(): + """ + 查询可交互的渠道 + """ + return [info.get("search_type") + for info in ModuleConf.MESSAGE_CONF.get('client').values() + if info.get('search_type')] diff --git a/app/message/message_center.py b/app/message/message_center.py new file mode 100644 index 0000000..80b477d --- /dev/null +++ b/app/message/message_center.py @@ -0,0 +1,59 @@ +import datetime +import time +from collections import deque + +from app.utils.commons import singleton + + +@singleton +class MessageCenter: + _message_queue = deque(maxlen=50) + _message_index = 0 + + def __init__(self): + pass + + def insert_system_message(self, level, title, content=None): + """ + 新增系统消息 + :param level: 级别 + :param title: 标题 + :param content: 内容 + """ + if not level or not title: + return + if not content and title.find(":") != -1: + strings = title.split(":") + if strings and len(strings) > 1: + title = strings[0] + content = strings[1] + title = title.replace("\n", "
").strip() if title else "" + content = content.replace("\n", "
").strip() if content else "" + self.__append_message_queue(level, title, content) + + def __append_message_queue(self, level, title, content): + """ + 将消息增加到队列 + """ + self._message_queue.appendleft({"level": level, + "title": title, + "content": content, + "time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}) + + def get_system_messages(self, num=20, lst_time=None): + """ + 查询系统消息 + :param num:条数 + :param lst_time: 最后时间 + """ + if not lst_time: + return list(self._message_queue)[-num:] + else: + ret_messages = [] + for message in list(self._message_queue): + if (datetime.datetime.strptime(message.get("time"), '%Y-%m-%d %H:%M:%S') - datetime.datetime.strptime( + lst_time, '%Y-%m-%d %H:%M:%S')).seconds > 0: + ret_messages.append(message) + else: + break + return ret_messages diff --git a/app/rss.py b/app/rss.py new file mode 100644 index 0000000..f22de7f --- /dev/null +++ b/app/rss.py @@ -0,0 +1,628 @@ +import re +import xml.dom.minidom +from threading import Lock + +import log +from app.downloader import Downloader +from app.filter import Filter +from app.helper import DbHelper +from app.media import Media +from app.media.meta import MetaInfo +from app.sites import Sites +from app.subscribe import Subscribe +from app.utils import DomUtils, RequestUtils, StringUtils, ExceptionUtils, RssTitleUtils, Torrent +from app.utils.types import MediaType, SearchType + +lock = Lock() + + +class Rss: + _sites = [] + filter = None + media = None + downloader = None + searcher = None + dbhelper = None + subscribe = None + + def __init__(self): + self.media = Media() + self.downloader = Downloader() + self.sites = Sites() + self.filter = Filter() + self.dbhelper = DbHelper() + self.subscribe = Subscribe() + self.init_config() + + def init_config(self): + self._sites = self.sites.get_sites(rss=True) + + def rssdownload(self): + """ + RSS订阅检索下载入口,由定时服务调用 + """ + + if not self._sites: + return + + with lock: + log.info("【Rss】开始RSS订阅...") + + # 读取电影订阅 + rss_movies = self.subscribe.get_subscribe_movies(state='R') + if not rss_movies: + log.warn("【Rss】没有正在订阅的电影") + else: + log.info("【Rss】电影订阅清单:%s" + % " ".join('%s' % info.get("name") for _, info in rss_movies.items())) + # 读取电视剧订阅 + rss_tvs = self.subscribe.get_subscribe_tvs(state='R') + if not rss_tvs: + log.warn("【Rss】没有正在订阅的电视剧") + else: + log.info("【Rss】电视剧订阅清单:%s" + % " ".join('%s' % info.get("name") for _, info in rss_tvs.items())) + # 没有订阅退出 + if not rss_movies and not rss_tvs: + return + + # 获取有订阅的站点范围 + check_sites = [] + check_all = False + for rid, rinfo in rss_movies.items(): + rss_sites = rinfo.get("rss_sites") + if not rss_sites: + check_all = True + break + else: + check_sites += rss_sites + if not check_all: + for rid, rinfo in rss_tvs.items(): + rss_sites = rinfo.get("rss_sites") + if not rss_sites: + check_all = True + break + else: + check_sites += rss_sites + if check_all: + check_sites = [] + else: + check_sites = list(set(check_sites)) + + # 匹配到的资源列表 + rss_download_torrents = [] + # 缺失的资源详情 + rss_no_exists = {} + # 遍历站点资源 + for site_info in self._sites: + if not site_info: + continue + # 站点名称 + site_name = site_info.get("name") + # 没有订阅的站点中的不检索 + if check_sites and site_name not in check_sites: + continue + # 站点rss链接 + rss_url = site_info.get("rssurl") + if not rss_url: + log.info(f"【Rss】{site_name} 未配置rssurl,跳过...") + continue + site_cookie = site_info.get("cookie") + site_ua = site_info.get("ua") + # 是否解析种子详情 + site_parse = site_info.get("parse") + # 是否使用代理 + site_proxy = site_info.get("proxy") + # 使用的规则 + site_fliter_rule = site_info.get("rule") + # 开始下载RSS + log.info(f"【Rss】正在处理:{site_name}") + if site_info.get("pri"): + site_order = 100 - int(site_info.get("pri")) + else: + site_order = 0 + rss_acticles = self.parse_rssxml(rss_url) + if not rss_acticles: + log.warn(f"【Rss】{site_name} 未下载到数据") + continue + else: + log.info(f"【Rss】{site_name} 获取数据:{len(rss_acticles)}") + # 处理RSS结果 + res_num = 0 + for article in rss_acticles: + try: + # 种子名 + title = article.get('title') + # 种子链接 + enclosure = article.get('enclosure') + # 种子页面 + page_url = article.get('link') + # 种子大小 + size = article.get('size') + # 开始处理 + log.info(f"【Rss】开始处理:{title}") + # 检查这个种子是不是下过了 + if self.dbhelper.is_torrent_rssd(enclosure): + log.info(f"【Rss】{title} 已成功订阅过") + continue + # 识别种子名称,开始检索TMDB + media_info = MetaInfo(title=title) + cache_info = self.media.get_cache_info(media_info) + if cache_info.get("id"): + # 使用缓存信息 + media_info.tmdb_id = cache_info.get("id") + media_info.type = cache_info.get("type") + media_info.title = cache_info.get("title") + media_info.year = cache_info.get("year") + else: + # 重新查询TMDB + media_info = self.media.get_media_info(title=title) + if not media_info: + log.warn(f"【Rss】{title} 无法识别出媒体信息!") + continue + elif not media_info.tmdb_info: + log.info(f"【Rss】{title} 识别为 {media_info.get_name()} 未匹配到TMDB媒体信息") + # 大小及种子页面 + media_info.set_torrent_info(size=size, + page_url=page_url, + site=site_name, + site_order=site_order, + enclosure=enclosure) + # 检查种子是否匹配订阅,返回匹配到的订阅ID、是否洗版、总集数、上传因子、下载因子 + match_flag, match_msg, match_info = self.check_torrent_rss( + media_info=media_info, + rss_movies=rss_movies, + rss_tvs=rss_tvs, + site_filter_rule=site_fliter_rule, + site_cookie=site_cookie, + site_parse=site_parse, + site_ua=site_ua, + site_proxy=site_proxy) + for msg in match_msg: + log.info(f"【Rss】{msg}") + + # 未匹配 + if not match_flag: + continue + + # 非模糊匹配命中,检查本地情况,检查删除订阅 + if not match_info.get("fuzzy_match"): + # 匹配到订阅,如没有TMDB信息则重新查询 + if not media_info.tmdb_info and media_info.tmdb_id: + media_info.set_tmdb_info(self.media.get_tmdb_info(mtype=media_info.type, + tmdbid=media_info.tmdb_id)) + if not media_info.tmdb_info: + continue + # 非洗版时检查本地是否存在 + if not match_info.get("over_edition"): + if media_info.type == MediaType.MOVIE: + exist_flag, rss_no_exists, _ = self.downloader.check_exists_medias( + meta_info=media_info, + no_exists=rss_no_exists + ) + else: + # 从登记薄中获取缺失剧集 + season = 1 + if match_info.get("season"): + season = int(str(match_info.get("season")).replace("S", "")) + # 设定的总集数 + total_ep = match_info.get("total") + # 设定的开始集数 + current_ep = match_info.get("current_ep") + # 表登记的缺失集数 + episodes = self.subscribe.get_subscribe_tv_episodes(match_info.get("id")) + if episodes is None: + episodes = [] + if current_ep: + episodes = list(range(int(current_ep), int(total_ep) + 1)) + rss_no_exists[media_info.tmdb_id] = [ + { + "season": season, + "episodes": episodes, + "total_episodes": total_ep + } + ] + else: + rss_no_exists[media_info.tmdb_id] = [ + { + "season": season, + "episodes": episodes, + "total_episodes": total_ep + } + ] + # 检查本地媒体库情况 + exist_flag, library_no_exists, _ = self.downloader.check_exists_medias( + meta_info=media_info, + total_ep={season: total_ep} + ) + # 取交集做为缺失集 + rss_no_exists = Torrent.get_intersection_episodes(target=rss_no_exists, + source=library_no_exists, + title=media_info.tmdb_id) + if rss_no_exists.get(media_info.tmdb_id): + log.info("【Rss】%s 订阅缺失季集:%s" % ( + media_info.get_title_string(), + rss_no_exists.get(media_info.tmdb_id) + )) + # 本地已存在 + if exist_flag: + continue + # 洗版模式 + else: + # 洗版时季集不完整的资源不要 + if media_info.type != MediaType.MOVIE \ + and media_info.get_episode_list(): + log.info( + f"【Rss】{media_info.get_title_string()}{media_info.get_season_string()} " + f"正在洗版,过滤掉季集不完整的资源:{title}" + ) + continue + if not self.subscribe.check_subscribe_over_edition( + rtype=media_info.type, + rssid=match_info.get("id"), + res_order=match_info.get("res_order")): + log.info( + f"【Rss】{media_info.get_title_string()}{media_info.get_season_string()} " + f"正在洗版,跳过低优先级或同优先级资源:{title}" + ) + continue + # 模糊匹配 + else: + # 不做处理,直接下载 + pass + + # 设置种子信息 + media_info.set_torrent_info(res_order=match_info.get("res_order"), + filter_rule=match_info.get("filter_rule"), + over_edition=match_info.get("over_edition"), + download_volume_factor=match_info.get("download_volume_factor"), + upload_volume_factor=match_info.get("upload_volume_factor"), + rssid=match_info.get("id")) + # 设置下载参数 + media_info.set_download_info(download_setting=match_info.get("download_setting"), + save_path=match_info.get("save_path")) + # 插入数据库历史记录 + self.dbhelper.insert_rss_torrents(media_info) + # 加入下载列表 + if media_info not in rss_download_torrents: + rss_download_torrents.append(media_info) + res_num = res_num + 1 + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【Rss】处理RSS发生错误:%s" % str(e)) + continue + log.info("【Rss】%s 处理结束,匹配到 %s 个有效资源" % (site_name, res_num)) + log.info("【Rss】所有RSS处理结束,共 %s 个有效资源" % len(rss_download_torrents)) + # 开始择优下载 + self.download_rss_torrent(rss_download_torrents=rss_download_torrents, + rss_no_exists=rss_no_exists) + + @staticmethod + def parse_rssxml(url): + """ + 解析RSS订阅URL,获取RSS中的种子信息 + :param url: RSS地址 + :return: 种子信息列表 + """ + _special_title_sites = { + 'pt.keepfrds.com': RssTitleUtils.keepfriends_title + } + + # 开始处理 + ret_array = [] + if not url: + return [] + site_domain = StringUtils.get_url_domain(url) + try: + ret = RequestUtils().get_res(url) + if not ret: + return [] + ret.encoding = ret.apparent_encoding + except Exception as e2: + ExceptionUtils.exception_traceback(e2) + log.console(str(e2)) + return [] + if ret: + ret_xml = ret.text + try: + # 解析XML + dom_tree = xml.dom.minidom.parseString(ret_xml) + rootNode = dom_tree.documentElement + items = rootNode.getElementsByTagName("item") + for item in items: + try: + # 标题 + title = DomUtils.tag_value(item, "title", default="") + if not title: + continue + # 标题特殊处理 + if site_domain and site_domain in _special_title_sites: + title = _special_title_sites.get(site_domain)(title) + # 描述 + description = DomUtils.tag_value(item, "description", default="") + # 种子页面 + link = DomUtils.tag_value(item, "link", default="") + # 种子链接 + enclosure = DomUtils.tag_value(item, "enclosure", "url", default="") + if not enclosure and not link: + continue + # 部分RSS只有link没有enclosure + if not enclosure and link: + enclosure = link + link = None + # 大小 + size = DomUtils.tag_value(item, "enclosure", "length", default=0) + if size and str(size).isdigit(): + size = int(size) + else: + size = 0 + # 发布日期 + pubdate = DomUtils.tag_value(item, "pubDate", default="") + if pubdate: + # 转换为时间 + pubdate = StringUtils.get_time_stamp(pubdate) + # 返回对象 + tmp_dict = {'title': title, + 'enclosure': enclosure, + 'size': size, + 'description': description, + 'link': link, + 'pubdate': pubdate} + ret_array.append(tmp_dict) + except Exception as e1: + ExceptionUtils.exception_traceback(e1) + continue + except Exception as e2: + ExceptionUtils.exception_traceback(e2) + return ret_array + return ret_array + + def check_torrent_rss(self, + media_info, + rss_movies, + rss_tvs, + site_filter_rule, + site_cookie, + site_parse, + site_ua, + site_proxy): + """ + 判断种子是否命中订阅 + :param media_info: 已识别的种子媒体信息 + :param rss_movies: 电影订阅清单 + :param rss_tvs: 电视剧订阅清单 + :param site_filter_rule: 站点过滤规则 + :param site_cookie: 站点的Cookie + :param site_parse: 是否解析种子详情 + :param site_ua: 站点请求UA + :param site_proxy: 是否使用代理 + :return: 匹配到的订阅ID、是否洗版、总集数、匹配规则的资源顺序、上传因子、下载因子,匹配的季(电视剧) + """ + # 默认值 + # 匹配状态 0不在订阅范围内 -1不符合过滤条件 1匹配 + match_flag = False + # 匹配的rss信息 + match_msg = [] + match_rss_info = {} + # 上传因素 + upload_volume_factor = None + # 下载因素 + download_volume_factor = None + hit_and_run = False + + # 匹配电影 + if media_info.type == MediaType.MOVIE and rss_movies: + for rid, rss_info in rss_movies.items(): + rss_sites = rss_info.get('rss_sites') + # 过滤订阅站点 + if rss_sites and media_info.site not in rss_sites: + continue + # tmdbid或名称年份匹配 + name = rss_info.get('name') + year = rss_info.get('year') + tmdbid = rss_info.get('tmdbid') + fuzzy_match = rss_info.get('fuzzy_match') + # 非模糊匹配 + if not fuzzy_match: + # 有tmdbid时使用tmdbid匹配 + if tmdbid and not tmdbid.startswith("DB:"): + if str(media_info.tmdb_id) != str(tmdbid): + continue + else: + # 豆瓣年份与tmdb取向不同 + if year and str(media_info.year) not in [str(year), + str(int(year) + 1), + str(int(year) - 1)]: + continue + if name != media_info.title: + continue + # 模糊匹配 + else: + # 匹配年份 + if year and str(year) != str(media_info.year): + continue + # 匹配关键字或正则表达式 + search_title = f"{media_info.org_string} {media_info.title} {media_info.year}" + if not re.search(name, search_title, re.I) and name not in search_title: + continue + # 媒体匹配成功 + match_flag = True + match_rss_info = rss_info + + break + # 匹配电视剧 + elif rss_tvs: + # 匹配种子标题 + for rid, rss_info in rss_tvs.items(): + rss_sites = rss_info.get('rss_sites') + # 过滤订阅站点 + if rss_sites and media_info.site not in rss_sites: + continue + # 有tmdbid时精确匹配 + name = rss_info.get('name') + year = rss_info.get('year') + season = rss_info.get('season') + tmdbid = rss_info.get('tmdbid') + fuzzy_match = rss_info.get('fuzzy_match') + # 非模糊匹配 + if not fuzzy_match: + if tmdbid and not tmdbid.startswith("DB:"): + if str(media_info.tmdb_id) != str(tmdbid): + continue + else: + # 匹配年份,年份可以为空 + if year and str(year) != str(media_info.year): + continue + # 匹配名称 + if name != media_info.title: + continue + # 匹配季,季可以为空 + if season and season != media_info.get_season_string(): + continue + # 模糊匹配 + else: + # 匹配季,季可以为空 + if season and season != "S00" and season != media_info.get_season_string(): + continue + # 匹配年份 + if year and str(year) != str(media_info.year): + continue + # 匹配关键字或正则表达式 + search_title = f"{media_info.org_string} {media_info.title} {media_info.year}" + if not re.search(name, search_title, re.I) and name not in search_title: + continue + # 媒体匹配成功 + match_flag = True + match_rss_info = rss_info + break + # 名称匹配成功,开始过滤 + if match_flag: + # 解析种子详情 + if site_parse: + # 检测Free + torrent_attr = self.sites.check_torrent_attr(torrent_url=media_info.page_url, + cookie=site_cookie, + ua=site_ua, + proxy=site_proxy) + if torrent_attr.get('2xfree'): + download_volume_factor = 0.0 + upload_volume_factor = 2.0 + elif torrent_attr.get('free'): + download_volume_factor = 0.0 + upload_volume_factor = 1.0 + else: + upload_volume_factor = 1.0 + download_volume_factor = 1.0 + if torrent_attr.get('hr'): + hit_and_run = True + # 设置属性 + media_info.set_torrent_info(upload_volume_factor=upload_volume_factor, + download_volume_factor=download_volume_factor, + hit_and_run=hit_and_run) + # 订阅无过滤规则应用站点设置 + filter_rule = match_rss_info.get('filter_rule') or site_filter_rule + filter_dict = { + "restype": match_rss_info.get('filter_restype'), + "pix": match_rss_info.get('filter_pix'), + "team": match_rss_info.get('filter_team'), + "rule": filter_rule + } + match_filter_flag, res_order, match_filter_msg = self.filter.check_torrent_filter(meta_info=media_info, + filter_args=filter_dict) + if not match_filter_flag: + match_msg.append(match_filter_msg) + return False, match_msg, match_rss_info + else: + match_msg.append("%s 识别为 %s %s 匹配订阅成功" % ( + media_info.org_string, + media_info.get_title_string(), + media_info.get_season_episode_string())) + match_msg.append(f"种子描述:{media_info.subtitle}") + match_rss_info.update({ + "res_order": res_order, + "filter_rule": filter_rule, + "upload_volume_factor": upload_volume_factor, + "download_volume_factor": download_volume_factor}) + return True, match_msg, match_rss_info + else: + match_msg.append("%s 识别为 %s %s 不在订阅范围" % ( + media_info.org_string, + media_info.get_title_string(), + media_info.get_season_episode_string())) + return False, match_msg, match_rss_info + + def download_rss_torrent(self, rss_download_torrents, rss_no_exists): + """ + 根据缺失情况以及匹配到的结果选择下载种子 + """ + + if not rss_download_torrents: + return + + finished_rss_torrents = [] + updated_rss_torrents = [] + + def __finish_rss(download_item): + """ + 完成订阅 + """ + if not download_item: + return + if not download_item.rssid \ + or download_item.rssid in finished_rss_torrents: + return + finished_rss_torrents.append(download_item.rssid) + self.subscribe.finish_rss_subscribe(rssid=download_item.rssid, + media=download_item) + + def __update_tv_rss(download_item, left_media): + """ + 更新订阅集数 + """ + if not download_item or not left_media: + return + if not download_item.rssid \ + or download_item.rssid in updated_rss_torrents: + return + updated_rss_torrents.append(download_item.rssid) + self.subscribe.update_subscribe_tv_lack(rssid=download_item.rssid, + media_info=download_item, + seasoninfo=left_media) + + def __update_over_edition(download_item): + """ + 更新洗版订阅 + """ + if not download_item: + return + if not download_item.rssid \ + or download_item.rssid in updated_rss_torrents: + return + if download_item.get_episode_list(): + return + updated_rss_torrents.append(download_item.rssid) + self.subscribe.update_subscribe_over_edition(rtype=download_item.type, + rssid=download_item.rssid, + media=download_item) + + # 去重择优后开始添加下载 + download_items, left_medias = self.downloader.batch_download(SearchType.RSS, + rss_download_torrents, + rss_no_exists) + # 批量删除订阅 + if download_items: + for item in download_items: + if not item.rssid: + continue + if item.over_edition: + # 更新洗版订阅 + __update_over_edition(item) + elif not left_medias or not left_medias.get(item.tmdb_id): + # 删除电视剧订阅 + __finish_rss(item) + else: + # 更新电视剧缺失剧集 + __update_tv_rss(item, left_medias.get(item.tmdb_id)) + log.info("【Rss】实际下载了 %s 个资源" % len(download_items)) + else: + log.info("【Rss】未下载到任何资源") diff --git a/app/rsschecker.py b/app/rsschecker.py new file mode 100644 index 0000000..8fda61a --- /dev/null +++ b/app/rsschecker.py @@ -0,0 +1,662 @@ +import json +import traceback + +import jsonpath +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.schedulers.background import BackgroundScheduler +from lxml import etree + +import log +from app.downloader import Downloader +from app.filter import Filter +from app.helper import DbHelper +from app.media import Media +from app.media.meta import MetaInfo +from app.message import Message +from app.searcher import Searcher +from app.subscribe import Subscribe +from app.utils import RequestUtils, StringUtils, ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import MediaType, SearchType +from config import Config + + +@singleton +class RssChecker(object): + message = None + searcher = None + filter = None + media = None + filterrule = None + downloader = None + subscribe = None + dbhelper = None + + _scheduler = None + _rss_tasks = [] + _rss_parsers = [] + _site_users = { + "D": "下载", + "R": "订阅", + "S": "搜索" + } + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.message = Message() + self.searcher = Searcher() + self.filter = Filter() + self.media = Media() + self.downloader = Downloader() + self.subscribe = Subscribe() + # 移除现有任务 + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + # 读取解析器列表 + rss_parsers = self.dbhelper.get_userrss_parser() + self._rss_parsers = [] + for rss_parser in rss_parsers: + self._rss_parsers.append( + { + "id": rss_parser.ID, + "name": rss_parser.NAME, + "type": rss_parser.TYPE, + "format": rss_parser.FORMAT, + "params": rss_parser.PARAMS, + "note": rss_parser.NOTE + } + ) + # 读取任务任务列表 + rsstasks = self.dbhelper.get_userrss_tasks() + self._rss_tasks = [] + for task in rsstasks: + parser = self.get_userrss_parser(task.PARSER) + if task.FILTER: + filterrule = self.filter.get_rule_groups(groupid=task.FILTER) + else: + filterrule = {} + # 解析属性 + note = {} + if task.NOTE: + try: + note = json.loads(task.NOTE) + except Exception as e: + print(str(e)) + note = {} + save_path = note.get("save_path") or "" + recognization = note.get("recognization") or "Y" + self._rss_tasks.append({ + "id": task.ID, + "name": task.NAME, + "address": task.ADDRESS, + "parser": task.PARSER, + "parser_name": parser.get("name") if parser else "", + "interval": task.INTERVAL, + "uses": task.USES if task.USES != "S" else "R", + "uses_text": self._site_users.get(task.USES), + "include": task.INCLUDE, + "exclude": task.EXCLUDE, + "filter": task.FILTER, + "filter_name": filterrule.get("name") if filterrule else "", + "update_time": task.UPDATE_TIME, + "counter": task.PROCESS_COUNT, + "state": task.STATE, + "save_path": task.SAVE_PATH or save_path, + "download_setting": task.DOWNLOAD_SETTING or "", + "recognization": task.RECOGNIZATION or recognization, + "over_edition": task.OVER_EDITION or 0, + "sites": json.loads(task.SITES) if task.SITES else {"rss_sites": [], "search_sites": []}, + "filter_args": json.loads(task.FILTER_ARGS) + if task.FILTER_ARGS else {"restype": "", "pix": "", "team": ""}, + }) + if not self._rss_tasks: + return + # 启动RSS任务 + self._scheduler = BackgroundScheduler(timezone=Config().get_timezone(), + executors={ + 'default': ThreadPoolExecutor(30) + }) + rss_flag = False + for task in self._rss_tasks: + if task.get("state") == "Y" and task.get("interval") and str(task.get("interval")).isdigit(): + rss_flag = True + self._scheduler.add_job(func=self.check_task_rss, + args=[task.get("id")], + trigger='interval', + seconds=int(task.get("interval")) * 60) + if rss_flag: + self._scheduler.print_jobs() + self._scheduler.start() + log.info("自定义订阅服务启动") + + def get_rsstask_info(self, taskid=None): + """ + 获取单个RSS任务详细信息 + """ + if taskid: + if str(taskid).isdigit(): + taskid = int(taskid) + for task in self._rss_tasks: + if task.get("id") == taskid: + return task + else: + return {} + return self._rss_tasks + + def check_task_rss(self, taskid): + """ + 处理自定义RSS任务,由定时服务调用 + :param taskid: 自定义RSS的ID + """ + if not taskid: + return + # 需要下载的项目 + rss_download_torrents = [] + # 需要订阅的项目 + rss_subscribe_torrents = [] + # 需要搜索的项目 + rss_search_torrents = [] + # 任务信息 + taskinfo = self.get_rsstask_info(taskid) + if not taskinfo: + return + rss_result = self.__parse_userrss_result(taskinfo) + if len(rss_result) == 0: + log.warn("【RssChecker】%s 未下载到数据" % taskinfo.get("name")) + return + else: + log.info("【RssChecker】%s 获取数据:%s" % (taskinfo.get("name"), len(rss_result))) + # 处理RSS结果 + res_num = 0 + no_exists = {} + for res in rss_result: + try: + # 种子名 + title = res.get('title') + if not title: + continue + # 种子链接 + enclosure = res.get('enclosure') + # 种子页面 + page_url = res.get('link') + # 种子大小 + size = StringUtils.str_filesize(res.get('size')) + # 年份 + year = res.get('year') + if year and len(year) > 4: + year = year[:4] + # 类型 + mediatype = res.get('type') + if mediatype: + mediatype = MediaType.MOVIE if mediatype == "movie" else MediaType.TV + + log.info("【RssChecker】开始处理:%s" % title) + + # 检查是不是处理过 + meta_name = "%s %s" % (title, year) if year else title + if self.dbhelper.is_userrss_finished(meta_name, enclosure): + log.info("【RssChecker】%s 已处理过" % title) + continue + + if taskinfo.get("uses") == "D": + # 识别种子名称,开始检索TMDB + media_info = MetaInfo(title=meta_name, + mtype=mediatype) + cache_info = self.media.get_cache_info(media_info) + if taskinfo.get("recognization") == "Y": + if cache_info.get("id"): + # 有缓存,直接使用缓存 + media_info.tmdb_id = cache_info.get("id") + media_info.type = cache_info.get("type") + media_info.title = cache_info.get("title") + media_info.year = cache_info.get("year") + else: + media_info = self.media.get_media_info(title=meta_name, + mtype=mediatype) + if not media_info: + log.warn("【RssChecker】%s 识别媒体信息出错!" % title) + continue + if not media_info.tmdb_info: + log.info("【RssChecker】%s 识别为 %s 未匹配到媒体信息" % (title, media_info.get_name())) + continue + # 检查是否已存在 + if media_info.type == MediaType.MOVIE: + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info, + no_exists=no_exists) + if exist_flag: + log.info("【RssChecker】电影 %s 已存在" % media_info.get_title_string()) + continue + else: + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info, + no_exists=no_exists) + # 当前剧集已存在,跳过 + if exist_flag: + # 已全部存在 + if not no_exists or not no_exists.get( + media_info.tmdb_id): + log.info("【RssChecker】电视剧 %s %s 已存在" % ( + media_info.get_title_string(), media_info.get_season_episode_string())) + continue + if no_exists.get(media_info.tmdb_id): + log.info("【RssChecker】%s 缺失季集:%s" + % (media_info.get_title_string(), no_exists.get(media_info.tmdb_id))) + # 大小及种子页面 + media_info.set_torrent_info(size=size, + page_url=page_url, + site=taskinfo.get("name"), + enclosure=enclosure) + # 检查种子是否匹配过滤条件 + filter_args = { + "include": taskinfo.get("include"), + "exclude": taskinfo.get("exclude"), + "rule": taskinfo.get("filter") + } + match_flag, res_order, match_msg = self.filter.check_torrent_filter(meta_info=media_info, + filter_args=filter_args) + # 未匹配 + if not match_flag: + log.info(f"【RssChecker】{match_msg}") + continue + else: + # 匹配优先级 + media_info.set_torrent_info(res_order=res_order) + if taskinfo.get("recognization") == "Y": + log.info("【RssChecker】%s 识别为 %s %s 匹配成功" % ( + title, + media_info.get_title_string(), + media_info.get_season_episode_string())) + # 补充TMDB完整信息 + if not media_info.tmdb_info: + media_info.set_tmdb_info(self.media.get_tmdb_info(mtype=media_info.type, + tmdbid=media_info.tmdb_id)) + # TMDB信息插入订阅任务 + if media_info.type != MediaType.MOVIE: + self.dbhelper.insert_userrss_mediainfos(taskid, media_info) + else: + log.info(f"【RssChecker】{title} 匹配成功") + # 添加下载列表 + if not enclosure: + log.warn("【RssChecker】%s RSS报文中没有enclosure种子链接" % taskinfo.get("name")) + continue + if media_info not in rss_download_torrents: + rss_download_torrents.append(media_info) + res_num = res_num + 1 + elif taskinfo.get("uses") == "R": + media_info = MetaInfo(title=meta_name, mtype=mediatype) + # 检查种子是否匹配过滤条件 + filter_args = { + "include": taskinfo.get("include"), + "exclude": taskinfo.get("exclude"), + "rule": -1 + + } + match_flag, _, match_msg = self.filter.check_torrent_filter(meta_info=media_info, + filter_args=filter_args) + # 未匹配 + if not match_flag: + log.info(f"【RssChecker】{match_msg}") + continue + # 添加订阅列表 + self.dbhelper.insert_rss_torrents(media_info) + if media_info not in rss_subscribe_torrents: + rss_subscribe_torrents.append(media_info) + res_num = res_num + 1 + else: + continue + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【RssChecker】处理RSS发生错误:%s - %s" % (str(e), traceback.format_exc())) + continue + log.info("【RssChecker】%s 处理结束,匹配到 %s 个有效资源" % (taskinfo.get("name"), res_num)) + # 添加下载 + if rss_download_torrents: + for media in rss_download_torrents: + ret, ret_msg = self.downloader.download(media_info=media, + download_dir=taskinfo.get("save_path"), + download_setting=taskinfo.get("download_setting")) + if ret: + self.message.send_download_message(in_from=SearchType.USERRSS, + can_item=media) + # 下载类型的 这里下载成功了 插入数据库 + self.dbhelper.insert_rss_torrents(media) + # 登记自定义RSS任务下载记录 + downloader = self.downloader.get_default_client_type().value + if media.download_setting: + download_attr = self.downloader.get_download_setting(media.download_setting) + if download_attr.get("downloader"): + downloader = download_attr.get("downloader") + self.dbhelper.insert_userrss_task_history(taskid, media.org_string, downloader) + else: + log.error("【RssChecker】添加下载任务 %s 失败:%s" % ( + media.get_title_string(), ret_msg or "请检查下载任务是否已存在")) + if ret_msg: + self.message.send_download_fail_message(media, ret_msg) + # 添加订阅 + if rss_subscribe_torrents: + for media in rss_subscribe_torrents: + code, msg, rss_media = self.subscribe.add_rss_subscribe( + mtype=media.type, + name=media.get_name(), + year=media.year, + season=media.begin_season, + rss_sites=taskinfo.get("sites", {}).get("rss_sites"), + search_sites=taskinfo.get("sites", {}).get("search_sites"), + over_edition=True if taskinfo.get("over_edition") else False, + filter_restype=taskinfo.get("filter_args", {}).get("restype"), + filter_pix=taskinfo.get("filter_args", {}).get("pix"), + filter_team=taskinfo.get("filter_args", {}).get("team"), + filter_rule=taskinfo.get("filter"), + save_path=taskinfo.get("save_path"), + download_setting=taskinfo.get("download_setting"), + ) + if rss_media and code == 0: + self.message.send_rss_success_message(in_from=SearchType.USERRSS, media_info=rss_media) + else: + log.warn("【RssChecker】%s 添加订阅失败:%s" % (media.get_name(), msg)) + + # 更新状态 + counter = len(rss_download_torrents) + len(rss_subscribe_torrents) + len(rss_search_torrents) + if counter: + self.dbhelper.update_userrss_task_info(taskid, counter) + + def __parse_userrss_result(self, taskinfo): + """ + 获取RSS链接数据,根据PARSER进行解析获取返回结果 + """ + rss_parser = self.get_userrss_parser(taskinfo.get("parser")) + if not rss_parser: + log.error("【RssChecker】任务 %s 的解析配置不存在" % taskinfo.get("name")) + return [] + if not rss_parser.get("format"): + log.error("【RssChecker】任务 %s 的解析配置不正确" % taskinfo.get("name")) + return [] + try: + rss_parser_format = json.loads(rss_parser.get("format")) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【RssChecker】任务 %s 的解析配置不是合法的Json格式" % taskinfo.get("name")) + return [] + # 拼装链接 + rss_url = taskinfo.get("address") + if not rss_url: + return [] + if rss_parser.get("params"): + _dict = { + "TMDBKEY": Config().get_config("app").get("rmt_tmdbkey") + } + try: + param_url = rss_parser.get("params").format(**_dict) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【RssChecker】任务 %s 的解析配置附加参数不合法" % taskinfo.get("name")) + return [] + rss_url = "%s?%s" % (rss_url, param_url) if rss_url.find("?") == -1 else "%s&%s" % (rss_url, param_url) + # 请求数据 + try: + ret = RequestUtils().get_res(rss_url) + if not ret: + return [] + ret.encoding = ret.apparent_encoding + except Exception as e2: + ExceptionUtils.exception_traceback(e2) + return [] + # 解析数据 XPATH + rss_result = [] + if rss_parser.get("type") == "XML": + try: + result_tree = etree.XML(ret.text.encode("utf-8")) + item_list = result_tree.xpath(rss_parser_format.get("list")) or [] + for item in item_list: + rss_item = {} + for key, attr in rss_parser_format.get("item", {}).items(): + if attr.get("path"): + if attr.get("namespaces"): + value = item.xpath("//ns:%s" % attr.get("path"), + namespaces={"ns": attr.get("namespaces")}) + else: + value = item.xpath(attr.get("path")) + elif attr.get("value"): + value = attr.get("value") + else: + continue + if value: + rss_item.update({key: value[0]}) + rss_result.append(rss_item) + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【RssChecker】任务 %s 获取的订阅报文无法解析:%s" % (taskinfo.get("name"), str(err))) + return [] + elif rss_parser.get("type") == "JSON": + try: + result_json = json.loads(ret.text) + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("【RssChecker】任务 %s 获取的订阅报文不是合法的Json格式:%s" % (taskinfo.get("name"), str(err))) + return [] + item_list = jsonpath.jsonpath(result_json, rss_parser_format.get("list"))[0] + if not isinstance(item_list, list): + log.error("【RssChecker】任务 %s 获取的订阅报文list后不是列表" % taskinfo.get("name")) + return [] + for item in item_list: + rss_item = {} + for key, attr in rss_parser_format.get("item", {}).items(): + if attr.get("path"): + value = jsonpath.jsonpath(item, attr.get("path")) + elif attr.get("value"): + value = attr.get("value") + else: + continue + if value: + rss_item.update({key: value[0]}) + rss_result.append(rss_item) + return rss_result + + def get_userrss_parser(self, pid=None): + if pid: + for rss_parser in self._rss_parsers: + if rss_parser.get("id") == int(pid): + return rss_parser + return {} + else: + return self._rss_parsers + + def get_rss_articles(self, taskid): + """ + 查看自定义RSS报文 + :param taskid: 自定义RSS的ID + """ + if not taskid: + return + # 下载订阅的文章列表 + rss_articles = [] + # 任务信息 + taskinfo = self.get_rsstask_info(taskid) + if not taskinfo: + return + rss_result = self.__parse_userrss_result(taskinfo) + if len(rss_result) == 0: + return [] + for res in rss_result: + try: + # 种子名 + title = res.get('title') + if not title: + continue + # 种子链接 + enclosure = res.get('enclosure') + # 种子页面 + link = res.get('link') + # 副标题 + description = res.get('description') + # 种子大小 + size = StringUtils.str_filesize(res.get('size')) + # 发布日期 + date = StringUtils.unify_datetime_str(res.get('date')) + # 年份 + year = res.get('year') + if year and len(year) > 4: + year = year[:4] + # 检查是不是处理过 + meta_name = "%s %s" % (title, year) if year else title + finish_flag = self.dbhelper.is_userrss_finished(meta_name, enclosure) + # 信息聚合 + params = { + "title": title, + "link": link, + "enclosure": enclosure, + "size": size, + "description": description, + "date": date, + "finish_flag": finish_flag, + } + if params not in rss_articles: + rss_articles.append(params) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【RssChecker】获取RSS报文发生错误:%s - %s" % (str(e), traceback.format_exc())) + return rss_articles + + def test_rss_articles(self, taskid, title): + """ + 测试RSS报文 + :param taskid: 自定义RSS的ID + :param title: RSS报文title + """ + # 任务信息 + taskinfo = self.get_rsstask_info(taskid) + if not taskinfo: + return + # 识别种子名称,开始检索TMDB + media_info = MetaInfo(title=title) + cache_info = self.media.get_cache_info(media_info) + if cache_info.get("id"): + # 有缓存,直接使用缓存 + media_info.tmdb_id = cache_info.get("id") + media_info.type = cache_info.get("type") + media_info.title = cache_info.get("title") + media_info.year = cache_info.get("year") + else: + media_info = self.media.get_media_info(title=title) + if not media_info: + log.warn("【RssChecker】%s 识别媒体信息出错!" % title) + # 检查是否匹配 + filter_args = { + "include": taskinfo.get("include"), + "exclude": taskinfo.get("exclude"), + "rule": taskinfo.get("filter") if taskinfo.get("uses") == "D" else None + } + match_flag, res_order, match_msg = self.filter.check_torrent_filter(meta_info=media_info, + filter_args=filter_args) + # 未匹配 + if not match_flag: + log.info(f"【RssChecker】{match_msg}") + else: + log.info("【RssChecker】%s 识别为 %s %s 匹配成功" % ( + title, + media_info.get_title_string(), + media_info.get_season_episode_string())) + media_info.set_torrent_info(res_order=res_order) + # 检查是否已存在 + no_exists = {} + exist_flag = False + if not media_info.tmdb_id: + log.info("【RssChecker】%s 识别为 %s 未匹配到媒体信息" % (title, media_info.get_name())) + else: + if media_info.type == MediaType.MOVIE: + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info, + no_exists=no_exists) + if exist_flag: + log.info("【RssChecker】电影 %s 已存在" % media_info.get_title_string()) + else: + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info, + no_exists=no_exists) + if exist_flag: + # 已全部存在 + if not no_exists or not no_exists.get( + media_info.tmdb_id): + log.info("【RssChecker】电视剧 %s %s 已存在" % ( + media_info.get_title_string(), media_info.get_season_episode_string())) + if no_exists.get(media_info.tmdb_id): + log.info("【RssChecker】%s 缺失季集:%s" + % (media_info.get_title_string(), no_exists.get(media_info.tmdb_id))) + return media_info, match_flag, exist_flag + + def check_rss_articles(self, flag, articles): + """ + RSS报文处理设置 + :param flag: set_finished/set_unfinish + :param articles: 报文(title/enclosure) + """ + try: + if flag == "set_finished": + for article in articles: + title = article.get("title") + enclosure = article.get("enclosure") + if not self.dbhelper.is_userrss_finished(title, enclosure): + self.dbhelper.simple_insert_rss_torrents(title, enclosure) + elif flag == "set_unfinish": + for article in articles: + self.dbhelper.simple_delete_rss_torrents(article.get("title"), article.get("enclosure")) + else: + return False + return True + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【RssChecker】设置RSS报文状态时发生错误:%s - %s" % (str(e), traceback.format_exc())) + return False + + def download_rss_articles(self, taskid, articles): + """ + RSS报文下载 + :param taskid: 自定义RSS的ID + :param articles: 报文(title/enclosure) + """ + if not taskid: + return + # 任务信息 + taskinfo = self.get_rsstask_info(taskid) + if not taskinfo: + return + for article in articles: + media = self.media.get_media_info(title=article.get("title")) + media.set_torrent_info(enclosure=article.get("enclosure")) + ret, ret_msg = self.downloader.download(media_info=media, + download_dir=taskinfo.get("save_path"), + download_setting=taskinfo.get("download_setting")) + if ret: + self.message.send_download_message(in_from=SearchType.USERRSS, + can_item=media) + # 插入数据库 + self.dbhelper.insert_rss_torrents(media) + # 登记自定义RSS任务下载记录 + downloader = self.downloader.get_default_client_type().value + if taskinfo.get("download_setting"): + download_attr = self.downloader.get_download_setting(taskinfo.get("download_setting")) + if download_attr.get("downloader"): + downloader = download_attr.get("downloader") + self.dbhelper.insert_userrss_task_history(taskid, media.org_string, downloader) + else: + log.error("【RssChecker】添加下载任务 %s 失败:%s" % ( + media.get_title_string(), ret_msg or "请检查下载任务是否已存在")) + if ret_msg: + self.message.send_download_fail_message(media, ret_msg) + return False + return True + + def get_userrss_mediainfos(self): + taskinfos = self.dbhelper.get_userrss_tasks() + mediainfos_all = [] + for taskinfo in taskinfos: + mediainfos = json.loads(taskinfo.MEDIAINFOS) if taskinfo.MEDIAINFOS else [] + if mediainfos: + mediainfos_all += mediainfos + return mediainfos_all diff --git a/app/scheduler.py b/app/scheduler.py new file mode 100644 index 0000000..4b09dea --- /dev/null +++ b/app/scheduler.py @@ -0,0 +1,265 @@ +import datetime +import math +import random +import traceback + +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.schedulers.background import BackgroundScheduler + +import log +from app.doubansync import DoubanSync +from app.downloader import Downloader +from app.helper import MetaHelper +from app.mediaserver import MediaServer +from app.rss import Rss +from app.sites import Sites, SiteUserInfo, SiteSignin +from app.subscribe import Subscribe +from app.sync import Sync +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from config import PT_TRANSFER_INTERVAL, METAINFO_SAVE_INTERVAL, \ + SYNC_TRANSFER_INTERVAL, RSS_CHECK_INTERVAL, REFRESH_PT_DATA_INTERVAL, \ + RSS_REFRESH_TMDB_INTERVAL, META_DELETE_UNKNOWN_INTERVAL, REFRESH_WALLPAPER_INTERVAL, Config +from web.backend.wallpaper import get_login_wallpaper + + +@singleton +class Scheduler: + SCHEDULER = None + _pt = None + _douban = None + _media = None + + def __init__(self): + self.init_config() + + def init_config(self): + self._pt = Config().get_config('pt') + self._media = Config().get_config('media') + self._douban = Config().get_config('douban') + + def run_service(self): + """ + 读取配置,启动定时服务 + """ + self.SCHEDULER = BackgroundScheduler(timezone=Config().get_timezone(), + executors={ + 'default': ThreadPoolExecutor(20) + }) + if not self.SCHEDULER: + return + if self._pt: + # 站点签到 + ptsignin_cron = str(self._pt.get('ptsignin_cron')) + if ptsignin_cron: + if '-' in ptsignin_cron: + try: + time_range = ptsignin_cron.split("-") + start_time_range_str = time_range[0] + end_time_range_str = time_range[1] + start_time_range_array = start_time_range_str.split(":") + end_time_range_array = end_time_range_str.split(":") + start_hour = int(start_time_range_array[0]) + start_minute = int(start_time_range_array[1]) + end_hour = int(end_time_range_array[0]) + end_minute = int(end_time_range_array[1]) + + def start_random_job(): + task_time_count = random.randint(start_hour * 60 + start_minute, end_hour * 60 + end_minute) + self.start_data_site_signin_job(math.floor(task_time_count / 60), task_time_count % 60) + + self.SCHEDULER.add_job(start_random_job, + "cron", + hour=start_hour, + minute=start_minute) + log.info("站点自动签到服务时间范围随机模式启动,起始时间于%s:%s" % ( + str(start_hour).rjust(2, '0'), str(start_minute).rjust(2, '0'))) + except Exception as e: + log.info("站点自动签到时间 时间范围随机模式 配置格式错误:%s %s" % (ptsignin_cron, str(e))) + elif ptsignin_cron.find(':') != -1: + try: + hour = int(ptsignin_cron.split(":")[0]) + minute = int(ptsignin_cron.split(":")[1]) + except Exception as e: + log.info("站点自动签到时间 配置格式错误:%s" % str(e)) + hour = minute = 0 + self.SCHEDULER.add_job(SiteSignin().signin, + "cron", + hour=hour, + minute=minute) + log.info("站点自动签到服务启动") + else: + try: + hours = float(ptsignin_cron) + except Exception as e: + log.info("站点自动签到时间 配置格式错误:%s" % str(e)) + hours = 0 + if hours: + self.SCHEDULER.add_job(SiteSignin().signin, + "interval", + hours=hours) + log.info("站点自动签到服务启动") + + # 下载文件转移 + pt_monitor = self._pt.get('pt_monitor') + if pt_monitor: + self.SCHEDULER.add_job(Downloader().transfer, 'interval', seconds=PT_TRANSFER_INTERVAL) + log.info("下载文件转移服务启动") + + # RSS下载器 + pt_check_interval = self._pt.get('pt_check_interval') + if pt_check_interval: + if isinstance(pt_check_interval, str) and pt_check_interval.isdigit(): + pt_check_interval = int(pt_check_interval) + else: + try: + pt_check_interval = round(float(pt_check_interval)) + except Exception as e: + log.error("RSS订阅周期 配置格式错误:%s" % str(e)) + pt_check_interval = 0 + if pt_check_interval: + if pt_check_interval < 300: + pt_check_interval = 300 + self.SCHEDULER.add_job(Rss().rssdownload, 'interval', seconds=pt_check_interval) + log.info("RSS订阅服务启动") + + # RSS订阅定时检索 + search_rss_interval = self._pt.get('search_rss_interval') + if search_rss_interval: + if isinstance(search_rss_interval, str) and search_rss_interval.isdigit(): + search_rss_interval = int(search_rss_interval) + else: + try: + search_rss_interval = round(float(search_rss_interval)) + except Exception as e: + log.error("订阅定时搜索周期 配置格式错误:%s" % str(e)) + search_rss_interval = 0 + if search_rss_interval: + if search_rss_interval < 6: + search_rss_interval = 6 + self.SCHEDULER.add_job(Subscribe().subscribe_search_all, 'interval', hours=search_rss_interval) + log.info("订阅定时搜索服务启动") + + # 豆瓣电影同步 + if self._douban: + douban_interval = self._douban.get('interval') + if douban_interval: + if isinstance(douban_interval, str): + if douban_interval.isdigit(): + douban_interval = int(douban_interval) + else: + try: + douban_interval = float(douban_interval) + except Exception as e: + log.info("豆瓣同步服务启动失败:%s" % str(e)) + douban_interval = 0 + if douban_interval: + self.SCHEDULER.add_job(DoubanSync().sync, 'interval', hours=douban_interval) + log.info("豆瓣同步服务启动") + + # 媒体库同步 + if self._media: + mediasync_interval = self._media.get("mediasync_interval") + if mediasync_interval: + if isinstance(mediasync_interval, str): + if mediasync_interval.isdigit(): + mediasync_interval = int(mediasync_interval) + else: + try: + mediasync_interval = round(float(mediasync_interval)) + except Exception as e: + log.info("豆瓣同步服务启动失败:%s" % str(e)) + mediasync_interval = 0 + if mediasync_interval: + self.SCHEDULER.add_job(MediaServer().sync_mediaserver, 'interval', hours=mediasync_interval) + log.info("媒体库同步服务启动") + + # 元数据定时保存 + self.SCHEDULER.add_job(MetaHelper().save_meta_data, 'interval', seconds=METAINFO_SAVE_INTERVAL) + + # 定时把队列中的监控文件转移走 + self.SCHEDULER.add_job(Sync().transfer_mon_files, 'interval', seconds=SYNC_TRANSFER_INTERVAL) + + # RSS队列中检索 + self.SCHEDULER.add_job(Subscribe().subscribe_search, 'interval', seconds=RSS_CHECK_INTERVAL) + + # 站点数据刷新 + self.SCHEDULER.add_job(SiteUserInfo().refresh_pt_date_now, + 'interval', + hours=REFRESH_PT_DATA_INTERVAL, + next_run_time=datetime.datetime.now() + datetime.timedelta(minutes=1)) + + # 豆瓣RSS转TMDB,定时更新TMDB数据 + self.SCHEDULER.add_job(Subscribe().refresh_rss_metainfo, 'interval', hours=RSS_REFRESH_TMDB_INTERVAL) + + # 定时清除未识别的缓存 + self.SCHEDULER.add_job(MetaHelper().delete_unknown_meta, 'interval', hours=META_DELETE_UNKNOWN_INTERVAL) + + # 定时刷新壁纸 + self.SCHEDULER.add_job(get_login_wallpaper, + 'interval', + hours=REFRESH_WALLPAPER_INTERVAL, + next_run_time=datetime.datetime.now()) + + self.SCHEDULER.print_jobs() + + self.SCHEDULER.start() + + def stop_service(self): + """ + 停止定时服务 + """ + try: + if self.SCHEDULER: + self.SCHEDULER.remove_all_jobs() + self.SCHEDULER.shutdown() + self.SCHEDULER = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + + def start_data_site_signin_job(self, hour, minute): + year = datetime.datetime.now().year + month = datetime.datetime.now().month + day = datetime.datetime.now().day + # 随机数从1秒开始,不在整点签到 + second = random.randint(1, 59) + log.info("站点自动签到时间 即将在%s-%s-%s,%s:%s:%s签到" % ( + str(year), str(month), str(day), str(hour), str(minute), str(second))) + if hour < 0 or hour > 24: + hour = -1 + if minute < 0 or minute > 60: + minute = -1 + if hour < 0 or minute < 0: + log.warn("站点自动签到时间 配置格式错误:不启动任务") + return + self.SCHEDULER.add_job(SiteSignin().signin, + "date", + run_date=datetime.datetime(year, month, day, hour, minute, second)) + + +def run_scheduler(): + """ + 启动定时服务 + """ + try: + Scheduler().run_service() + except Exception as err: + log.error("启动定时服务失败:%s - %s" % (str(err), traceback.format_exc())) + + +def stop_scheduler(): + """ + 停止定时服务 + """ + try: + Scheduler().stop_service() + except Exception as err: + log.debug("停止定时服务失败:%s" % str(err)) + + +def restart_scheduler(): + """ + 重启定时服务 + """ + stop_scheduler() + run_scheduler() diff --git a/app/searcher.py b/app/searcher.py new file mode 100644 index 0000000..f4a828c --- /dev/null +++ b/app/searcher.py @@ -0,0 +1,181 @@ +import log +from app.helper import DbHelper +from app.indexer import Indexer +from config import Config +from app.message import Message +from app.downloader import Downloader +from app.media import Media +from app.helper import ProgressHelper +from app.utils.types import SearchType + + +class Searcher: + downloader = None + media = None + message = None + indexer = None + progress = None + dbhelper = None + + _search_auto = True + + def __init__(self): + self.downloader = Downloader() + self.media = Media() + self.message = Message() + self.progress = ProgressHelper() + self.dbhelper = DbHelper() + self.indexer = Indexer() + self.init_config() + + def init_config(self): + self._search_auto = Config().get_config("pt").get('search_auto', True) + + def search_medias(self, + key_word: [str, list], + filter_args: dict, + match_media=None, + in_from: SearchType = None): + """ + 根据关键字调用索引器检查媒体 + :param key_word: 检索的关键字,不能为空 + :param filter_args: 过滤条件 + :param match_media: 区配的媒体信息 + :param in_from: 搜索渠道 + :return: 命中的资源媒体信息列表 + """ + if not key_word: + return [] + if not self.indexer: + return [] + return self.indexer.search_by_keyword(key_word=key_word, + filter_args=filter_args, + match_media=match_media, + in_from=in_from) + + def search_one_media(self, media_info, + in_from: SearchType, + no_exists: dict, + sites: list = None, + filters: dict = None, + user_name=None): + """ + 只检索和下载一个资源,用于精确检索下载,由微信、Telegram或豆瓣调用 + :param media_info: 已识别的媒体信息 + :param in_from: 搜索渠道 + :param no_exists: 缺失的剧集清单 + :param sites: 检索哪些站点 + :param filters: 过滤条件,为空则不过滤 + :param user_name: 用户名 + :return: 请求的资源是否全部下载完整,如完整则返回媒体信息 + 请求的资源如果是剧集则返回下载后仍然缺失的季集信息 + 搜索到的结果数量 + 下载到的结果数量,如为None则表示未开启自动下载 + """ + if not media_info: + return None, {}, 0, 0 + # 进度计数重置 + self.progress.start('search') + # 查找的季 + if media_info.begin_season is None: + search_season = None + else: + search_season = media_info.get_season_list() + # 查找的集 + search_episode = media_info.get_episode_list() + if search_episode and not search_season: + search_season = [1] + # 过滤条件 + filter_args = {"season": search_season, + "episode": search_episode, + "year": media_info.year, + "type": media_info.type, + "site": sites, + "seeders": True} + if filters: + filter_args.update(filters) + if media_info.keyword: + # 直接使用搜索词搜索 + first_search_name = media_info.keyword + second_search_name = None + else: + # 中文名 + if media_info.cn_name: + search_cn_name = media_info.cn_name + else: + search_cn_name = media_info.title + # 英文名 + search_en_name = None + if media_info.en_name: + search_en_name = media_info.en_name + else: + if media_info.original_language == "en": + search_en_name = media_info.original_title + else: + # 此处使用独立对象,避免影响TMDB语言 + en_title = Media().get_tmdb_en_title(media_info) + if en_title: + search_en_name = en_title + # 两次搜索名称 + second_search_name = None + if Config().get_config("laboratory").get("search_en_title"): + if search_en_name: + first_search_name = search_en_name + second_search_name = search_cn_name + else: + first_search_name = search_cn_name + else: + first_search_name = search_cn_name + if search_en_name: + second_search_name = search_en_name + # 开始搜索 + log.info("【Searcher】开始检索 %s ..." % first_search_name) + media_list = self.search_medias(key_word=first_search_name, + filter_args=filter_args, + match_media=media_info, + in_from=in_from) + # 使用名称重新搜索 + if len(media_list) == 0 \ + and second_search_name \ + and second_search_name != first_search_name: + log.info("【Searcher】%s 未检索到资源,尝试通过 %s 重新检索 ..." % (first_search_name, second_search_name)) + media_list = self.search_medias(key_word=second_search_name, + filter_args=filter_args, + match_media=media_info, + in_from=in_from) + + if len(media_list) == 0: + log.info("【Searcher】%s 未搜索到任何资源" % second_search_name) + return None, no_exists, 0, 0 + else: + if in_from in self.message.get_search_types(): + # 保存搜索记录 + self.dbhelper.delete_all_search_torrents() + # 搜索结果排序 + media_list = sorted(media_list, key=lambda x: "%s%s%s%s" % (str(x.title).ljust(100, ' '), + str(x.res_order).rjust(3, '0'), + str(x.site_order).rjust(3, '0'), + str(x.seeders).rjust(10, '0')), + reverse=True) + # 插入数据库 + self.dbhelper.insert_search_results(media_list) + # 微信未开自动下载时返回 + if not self._search_auto: + return None, no_exists, len(media_list), None + # 择优下载 + download_items, left_medias = self.downloader.batch_download(in_from=in_from, + media_list=media_list, + need_tvs=no_exists, + user_name=user_name) + # 统计下载情况,下全了返回True,没下全返回False + if not download_items: + log.info("【Searcher】%s 未下载到资源" % media_info.title) + return None, left_medias, len(media_list), 0 + else: + log.info("【Searcher】实际下载了 %s 个资源" % len(download_items)) + # 还有剩下的缺失,说明没下完,返回False + if left_medias: + return None, left_medias, len(media_list), len(download_items) + # 全部下完了 + else: + return download_items[0], no_exists, len(media_list), len(download_items) diff --git a/app/sites/__init__.py b/app/sites/__init__.py new file mode 100644 index 0000000..8394b0a --- /dev/null +++ b/app/sites/__init__.py @@ -0,0 +1,4 @@ +from app.sites.site_userinfo import SiteUserInfo +from .sites import Sites +from .site_cookie import SiteCookie +from .site_signin import SiteSignin diff --git a/app/sites/site_cookie.py b/app/sites/site_cookie.py new file mode 100644 index 0000000..2b040eb --- /dev/null +++ b/app/sites/site_cookie.py @@ -0,0 +1,302 @@ +import base64 +import time + +from lxml import etree +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as es +from selenium.webdriver.support.wait import WebDriverWait + +import log +from app.helper import ChromeHelper, ProgressHelper, DbHelper, OcrHelper, SiteHelper +from app.sites.sites import Sites +from app.conf import SiteConf +from app.utils import StringUtils, RequestUtils, ExceptionUtils +from app.utils.commons import singleton + + +@singleton +class SiteCookie(object): + progress = None + sites = None + ocrhelper = None + dbhelpter = None + captcha_code = {} + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelpter = DbHelper() + self.progress = ProgressHelper() + self.sites = Sites() + self.ocrhelper = OcrHelper() + self.captcha_code = {} + + def set_code(self, code, value): + """ + 设置验证码的值 + """ + self.captcha_code[code] = value + + def get_code(self, code): + """ + 获取验证码的值 + """ + return self.captcha_code.get(code) + + def __get_site_cookie_ua(self, + url, + username, + password, + twostepcode=None, + ocrflag=False): + """ + 获取站点cookie和ua + :param url: 站点地址 + :param username: 用户名 + :param password: 密码 + :param twostepcode: 两步验证 + :param ocrflag: 是否开启OCR识别 + :return: cookie、ua、message + """ + if not url or not username or not password: + return None, None, "参数错误" + # 全局锁 + chrome = ChromeHelper() + if not chrome.get_status(): + return None, None, "需要浏览器内核环境才能更新站点信息" + if not chrome.visit(url=url): + return None, None, "Chrome模拟访问失败" + # 循环检测是否过cf + cloudflare = chrome.pass_cloudflare() + if not cloudflare: + return None, None, "跳转站点失败,无法通过Cloudflare验证" + # 登录页面代码 + html_text = chrome.get_html() + if not html_text: + return None, None, "获取源码失败" + if SiteHelper.is_logged_in(html_text): + return chrome.get_cookies(), chrome.get_ua(), "已经登录过且Cookie未失效" + # 查找用户名输入框 + html = etree.HTML(html_text) + username_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("username"): + if html.xpath(xpath): + username_xpath = xpath + break + if not username_xpath: + return None, None, "未找到用户名输入框" + # 查找密码输入框 + password_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("password"): + if html.xpath(xpath): + password_xpath = xpath + break + if not password_xpath: + return None, None, "未找到密码输入框" + # 查找两步验证码 + twostepcode_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("twostep"): + if html.xpath(xpath): + twostepcode_xpath = xpath + break + # 查找验证码输入框 + captcha_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("captcha"): + if html.xpath(xpath): + captcha_xpath = xpath + break + # 查找验证码图片 + captcha_img_url = None + if captcha_xpath: + for xpath in SiteConf.SITE_LOGIN_XPATH.get("captcha_img"): + if html.xpath(xpath): + captcha_img_url = html.xpath(xpath)[0] + break + if not captcha_img_url: + return None, None, "未找到验证码图片" + # 查找登录按钮 + submit_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("submit"): + if html.xpath(xpath): + submit_xpath = xpath + break + if not submit_xpath: + return None, None, "未找到登录按钮" + # 点击登录按钮 + try: + submit_obj = WebDriverWait(driver=chrome.browser, + timeout=6).until(es.element_to_be_clickable((By.XPATH, + submit_xpath))) + if submit_obj: + # 输入用户名 + chrome.browser.find_element(By.XPATH, username_xpath).send_keys(username) + # 输入密码 + chrome.browser.find_element(By.XPATH, password_xpath).send_keys(password) + # 输入两步验证码 + if twostepcode and twostepcode_xpath: + twostepcode_element = chrome.browser.find_element(By.XPATH, twostepcode_xpath) + if twostepcode_element.is_displayed(): + twostepcode_element.send_keys(twostepcode) + # 识别验证码 + if captcha_xpath: + captcha_element = chrome.browser.find_element(By.XPATH, captcha_xpath) + if captcha_element.is_displayed(): + code_url = self.__get_captcha_url(url, captcha_img_url) + if ocrflag: + # 自动OCR识别验证码 + captcha = self.get_captcha_text(chrome, code_url) + if captcha: + log.info("【Sites】验证码地址为:%s,识别结果:%s" % (code_url, captcha)) + else: + return None, None, "验证码识别失败" + else: + # 等待用户输入 + captcha = None + code_key = StringUtils.generate_random_str(5) + for sec in range(30, 0, -1): + if self.get_code(code_key): + # 用户输入了 + captcha = self.get_code(code_key) + log.info("【Sites】接收到验证码:%s" % captcha) + self.progress.update(ptype='sitecookie', + text="接收到验证码:%s" % captcha) + break + else: + # 获取验证码图片base64 + code_bin = self.get_captcha_base64(chrome, code_url) + if not code_bin: + return None, None, "获取验证码图片数据失败" + else: + code_bin = f"data:image/png;base64,{code_bin}" + # 推送到前端 + self.progress.update(ptype='sitecookie', + text=f"{code_bin}|{code_key}") + time.sleep(1) + if not captcha: + return None, None, "验证码输入超时" + # 输入验证码 + captcha_element.send_keys(captcha) + else: + # 不可见元素不处理 + pass + # 提交登录 + submit_obj.click() + else: + return None, None, "未找到登录按钮" + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None, None, "仿真登录失败:%s" % str(e) + # 登录后的源码 + html_text = chrome.get_html() + if not html_text: + return None, None, "获取源码失败" + if SiteHelper.is_logged_in(html_text): + return chrome.get_cookies(), chrome.get_ua(), "" + else: + # 读取错误信息 + error_xpath = None + for xpath in SiteConf.SITE_LOGIN_XPATH.get("error"): + if html.xpath(xpath): + error_xpath = xpath + break + if not error_xpath: + return None, None, "登录失败" + else: + error_msg = html.xpath(error_xpath)[0] + return None, None, error_msg + + def get_captcha_text(self, chrome, code_url): + """ + 识别验证码图片的内容 + """ + code_b64 = self.get_captcha_base64(chrome=chrome, + image_url=code_url) + if not code_b64: + return "" + return self.ocrhelper.get_captcha_text(image_b64=code_b64) + + @staticmethod + def __get_captcha_url(siteurl, imageurl): + """ + 获取验证码图片的URL + """ + if not siteurl or not imageurl: + return "" + if imageurl.startswith("/"): + imageurl = imageurl[1:] + return "%s/%s" % (StringUtils.get_base_url(siteurl), imageurl) + + def update_sites_cookie_ua(self, + username, + password, + twostepcode=None, + siteid=None, + ocrflag=False): + """ + 更新所有站点Cookie和ua + """ + # 获取站点列表 + sites = self.sites.get_sites(siteid=siteid) + if siteid: + sites = [sites] + # 总数量 + site_num = len(sites) + # 当前数量 + curr_num = 0 + # 返回码、返回消息 + retcode = 0 + messages = [] + # 开始进度 + self.progress.start('sitecookie') + for site in sites: + if not site.get("signurl") and not site.get("rssurl"): + log.info("【Sites】%s 未设置地址,跳过" % site.get("name")) + continue + log.info("【Sites】开始更新 %s Cookie和User-Agent ..." % site.get("name")) + self.progress.update(ptype='sitecookie', + text="开始更新 %s Cookie和User-Agent ..." % site.get("name")) + # 登录页面地址 + baisc_url = StringUtils.get_base_url(site.get("signurl") or site.get("rssurl")) + site_conf = self.sites.get_grapsite_conf(url=baisc_url) + if site_conf.get("LOGIN"): + login_url = "%s/%s" % (baisc_url, site_conf.get("LOGIN")) + else: + login_url = "%s/login.php" % baisc_url + # 获取Cookie和User-Agent + cookie, ua, msg = self.__get_site_cookie_ua(url=login_url, + username=username, + password=password, + twostepcode=twostepcode, + ocrflag=ocrflag) + # 更新进度 + curr_num += 1 + if not cookie: + log.error("【Sites】获取 %s 信息失败:%s" % (site.get("name"), msg)) + messages.append("%s %s" % (site.get("name"), msg)) + self.progress.update(ptype='sitecookie', + value=round(100 * (curr_num / site_num)), + text="%s %s" % (site.get("name"), msg)) + retcode = 1 + else: + self.dbhelpter.update_site_cookie_ua(site.get("id"), cookie, ua) + log.info("【Sites】更新 %s 的Cookie和User-Agent成功" % site.get("name")) + messages.append("%s %s" % (site.get("name"), msg or "更新Cookie和User-Agent成功")) + self.progress.update(ptype='sitecookie', + value=round(100 * (curr_num / site_num)), + text="%s %s" % (site.get("name"), msg or "更新Cookie和User-Agent成功")) + self.progress.end('sitecookie') + return retcode, messages + + @staticmethod + def get_captcha_base64(chrome, image_url): + """ + 根据图片地址,获取验证码图片base64编码 + """ + if not image_url: + return "" + ret = RequestUtils(headers=chrome.get_ua(), + cookies=chrome.get_cookies()).get_res(image_url) + if ret: + return base64.b64encode(ret.content).decode() + return "" diff --git a/app/sites/site_signin.py b/app/sites/site_signin.py new file mode 100644 index 0000000..127d88b --- /dev/null +++ b/app/sites/site_signin.py @@ -0,0 +1,166 @@ +import re +from multiprocessing.dummy import Pool as ThreadPool +from threading import Lock + +from lxml import etree +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as es +from selenium.webdriver.support.wait import WebDriverWait + +import log +from app.conf import SiteConf +from app.helper import ChromeHelper, SubmoduleHelper, DbHelper, SiteHelper +from app.message import Message +from app.sites.sites import Sites +from app.utils import RequestUtils, ExceptionUtils, StringUtils +from app.utils.commons import singleton +from config import Config + +lock = Lock() + + +@singleton +class SiteSignin(object): + sites = None + dbhelper = None + message = None + + _MAX_CONCURRENCY = 10 + + def __init__(self): + # 加载模块 + self._site_schema = SubmoduleHelper.import_submodules('app.sites.sitesignin', + filter_func=lambda _, obj: hasattr(obj, 'match')) + log.debug(f"【Sites】加载站点签到:{self._site_schema}") + self.init_config() + + def init_config(self): + self.sites = Sites() + self.dbhelper = DbHelper() + self.message = Message() + + def __build_class(self, url): + for site_schema in self._site_schema: + try: + if site_schema.match(url): + return site_schema + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + def signin(self): + """ + 站点并发签到 + """ + sites = self.sites.get_sites(signin=True) + if not sites: + return + with ThreadPool(min(len(sites), self._MAX_CONCURRENCY)) as p: + status = p.map(self.__signin_site, sites) + if status: + self.message.send_site_signin_message(status) + + def __signin_site(self, site_info): + """ + 签到一个站点 + """ + site_module = self.__build_class(site_info.get("signurl")) + if site_module: + return site_module.signin(site_info) + else: + return self.__signin_base(site_info) + + @staticmethod + def __signin_base(site_info): + """ + 通用签到处理 + :param site_info: 站点信息 + :return: 签到结果信息 + """ + if not site_info: + return "" + site = site_info.get("name") + try: + site_url = site_info.get("signurl") + site_cookie = site_info.get("cookie") + ua = site_info.get("ua") + if not site_url or not site_cookie: + log.warn("【Sites】未配置 %s 的站点地址或Cookie,无法签到" % str(site)) + return "" + chrome = ChromeHelper() + if site_info.get("chrome") and chrome.get_status(): + # 首页 + log.info("【Sites】开始站点仿真签到:%s" % site) + home_url = StringUtils.get_base_url(site_url) + if not chrome.visit(url=home_url, ua=ua, cookie=site_cookie): + log.warn("【Sites】%s 无法打开网站" % site) + return f"【{site}】无法打开网站!" + # 循环检测是否过cf + cloudflare = chrome.pass_cloudflare() + if not cloudflare: + log.warn("【Sites】%s 跳转站点失败" % site) + return f"【{site}】跳转站点失败!" + # 判断是否已签到 + html_text = chrome.get_html() + if not html_text: + log.warn("【Sites】%s 获取站点源码失败" % site) + return f"【{site}】获取站点源码失败!" + # 查找签到按钮 + html = etree.HTML(html_text) + xpath_str = None + for xpath in SiteConf.SITE_CHECKIN_XPATH: + if html.xpath(xpath): + xpath_str = xpath + break + if re.search(r'已签|签到已得', html_text, re.IGNORECASE) \ + and not xpath_str: + log.info("【Sites】%s 今日已签到" % site) + return f"【{site}】今日已签到" + if not xpath_str: + if SiteHelper.is_logged_in(html_text): + log.warn("【Sites】%s 未找到签到按钮,模拟登录成功" % site) + return f"【{site}】模拟登录成功" + else: + log.info("【Sites】%s 未找到签到按钮,且模拟登录失败" % site) + return f"【{site}】模拟登录失败!" + # 开始仿真 + try: + checkin_obj = WebDriverWait(driver=chrome.browser, timeout=6).until( + es.element_to_be_clickable((By.XPATH, xpath_str))) + if checkin_obj: + checkin_obj.click() + log.info("【Sites】%s 仿真签到成功" % site) + return f"【{site}】仿真签到成功" + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.warn("【Sites】%s 仿真签到失败:%s" % (site, str(e))) + return f"【{site}】签到失败!" + # 模拟登录 + else: + if site_url.find("attendance.php") != -1: + checkin_text = "签到" + else: + checkin_text = "模拟登录" + log.info(f"【Sites】开始站点{checkin_text}:{site}") + # 访问链接 + res = RequestUtils(cookies=site_cookie, + headers=ua, + proxies=Config().get_proxies() if site_info.get("proxy") else None + ).get_res(url=site_url) + if res and res.status_code == 200: + if not SiteHelper.is_logged_in(res.text): + log.warn(f"【Sites】{site} {checkin_text}失败,请检查Cookie") + return f"【{site}】{checkin_text}失败,请检查Cookie!" + else: + log.info(f"【Sites】{site} {checkin_text}成功") + return f"【{site}】{checkin_text}成功" + elif res is not None: + log.warn(f"【Sites】{site} {checkin_text}失败,状态码:{res.status_code}") + return f"【{site}】{checkin_text}失败,状态码:{res.status_code}!" + else: + log.warn(f"【Sites】{site} {checkin_text}失败,无法打开网站") + return f"【{site}】{checkin_text}失败,无法打开网站!" + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.warn("【Sites】%s 签到出错:%s" % (site, str(e))) + return f"{site} 签到出错:{str(e)}!" diff --git a/app/sites/site_userinfo.py b/app/sites/site_userinfo.py new file mode 100644 index 0000000..7411b10 --- /dev/null +++ b/app/sites/site_userinfo.py @@ -0,0 +1,366 @@ +import json +from datetime import datetime +from multiprocessing.dummy import Pool as ThreadPool +from threading import Lock + +import requests + +import log +from app.helper import ChromeHelper, SubmoduleHelper, DbHelper +from app.message import Message +from app.sites.sites import Sites +from app.utils import RequestUtils, ExceptionUtils +from app.utils.commons import singleton +from config import Config + +lock = Lock() + + +@singleton +class SiteUserInfo(object): + + sites = None + dbhelper = None + message = None + + _MAX_CONCURRENCY = 10 + _last_update_time = None + _sites_data = {} + + def __init__(self): + + # 加载模块 + self._site_schema = SubmoduleHelper.import_submodules('app.sites.siteuserinfo', + filter_func=lambda _, obj: hasattr(obj, 'schema')) + self._site_schema.sort(key=lambda x: x.order) + log.debug(f"【Sites】加载站点解析:{self._site_schema}") + self.init_config() + + def init_config(self): + self.sites = Sites() + self.dbhelper = DbHelper() + self.message = Message() + # 站点上一次更新时间 + self._last_update_time = None + # 站点数据 + self._sites_data = {} + + def __build_class(self, html_text): + for site_schema in self._site_schema: + try: + if site_schema.match(html_text): + return site_schema + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None + + def build(self, url, site_name, site_cookie=None, ua=None, emulate=None, proxy=False): + if not site_cookie: + return None + session = requests.Session() + log.debug(f"【Sites】站点 {site_name} url={url} site_cookie={site_cookie} ua={ua}") + # 检测环境,有浏览器内核的优先使用仿真签到 + chrome = ChromeHelper() + if emulate and chrome.get_status(): + if not chrome.visit(url=url, ua=ua, cookie=site_cookie): + log.error("【Sites】%s 无法打开网站" % site_name) + return None + # 循环检测是否过cf + cloudflare = chrome.pass_cloudflare() + if not cloudflare: + log.error("【Sites】%s 跳转站点失败" % site_name) + return None + # 判断是否已签到 + html_text = chrome.get_html() + else: + proxies = Config().get_proxies() if proxy else None + res = RequestUtils(cookies=site_cookie, + session=session, + headers=ua, + proxies=proxies + ).get_res(url=url) + if res and res.status_code == 200: + if "charset=utf-8" in res.text or "charset=UTF-8" in res.text: + res.encoding = "UTF-8" + else: + res.encoding = res.apparent_encoding + html_text = res.text + # 第一次登录反爬 + if html_text.find("title") == -1: + i = html_text.find("window.location") + if i == -1: + return None + tmp_url = url + html_text[i:html_text.find(";")] \ + .replace("\"", "").replace("+", "").replace(" ", "").replace("window.location=", "") + res = RequestUtils(cookies=site_cookie, + session=session, + headers=ua, + proxies=proxies + ).get_res(url=tmp_url) + if res and res.status_code == 200: + if "charset=utf-8" in res.text or "charset=UTF-8" in res.text: + res.encoding = "UTF-8" + else: + res.encoding = res.apparent_encoding + html_text = res.text + if not html_text: + return None + else: + log.error("【Sites】站点 %s 被反爬限制:%s, 状态码:%s" % (site_name, url, res.status_code)) + return None + + # 兼容假首页情况,假首页通常没有 0: + for head, date, content in site_user_info.message_unread_contents: + msg_title = f"【站点 {site_user_info.site_name} 消息】" + msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}" + self.message.send_site_message(title=msg_title, text=msg_text) + else: + self.message.send_site_message( + title=f"站点 {site_user_info.site_name} 收到 {site_user_info.message_unread} 条新消息,请登陆查看") + + def refresh_pt_date_now(self): + """ + 强制刷新站点数据 + """ + self.__refresh_all_site_data(force=True) + + def get_pt_date(self, specify_sites=None, force=False): + """ + 获取站点上传下载量 + """ + self.__refresh_all_site_data(force=force, specify_sites=specify_sites) + return self._sites_data + + def __refresh_all_site_data(self, force=False, specify_sites=None): + """ + 多线程刷新站点下载上传量,默认间隔6小时 + """ + if not self.sites.get_sites(): + return + + with lock: + + if not force \ + and not specify_sites \ + and self._last_update_time \ + and (datetime.now() - self._last_update_time).seconds < 6 * 3600: + return + + if specify_sites \ + and not isinstance(specify_sites, list): + specify_sites = [specify_sites] + + # 没有指定站点,默认使用全部站点 + if not specify_sites: + refresh_sites = self.sites.get_sites(statistic=True) + else: + refresh_sites = [site for site in self.sites.get_sites(statistic=True) if + site.get("name") in specify_sites] + + if not refresh_sites: + return + + # 并发刷新 + with ThreadPool(min(len(refresh_sites), self._MAX_CONCURRENCY)) as p: + site_user_infos = p.map(self.__refresh_site_data, refresh_sites) + site_user_infos = [info for info in site_user_infos if info] + + # 登记历史数据 + self.dbhelper.insert_site_statistics_history(site_user_infos) + # 实时用户数据 + self.dbhelper.update_site_user_statistics(site_user_infos) + # 更新站点图标 + self.dbhelper.update_site_favicon(site_user_infos) + # 实时做种信息 + self.dbhelper.update_site_seed_info(site_user_infos) + # 站点图标重新加载 + self.sites.init_favicons() + + # 更新时间 + self._last_update_time = datetime.now() + + def get_pt_site_statistics_history(self, days=7): + """ + 获取站点上传下载量 + """ + site_urls = [] + for site in self.sites.get_sites(statistic=True): + site_url = site.get("strict_url") + if site_url: + site_urls.append(site_url) + + return self.dbhelper.get_site_statistics_recent_sites(days=days, strict_urls=site_urls) + + def get_site_user_statistics(self, sites=None, encoding="RAW"): + """ + 获取站点用户数据 + :param sites: 站点名称 + :param encoding: RAW/DICT + :return: + """ + statistic_sites = self.sites.get_sites(statistic=True) + if not sites: + site_urls = [site.get("strict_url") for site in statistic_sites] + else: + site_urls = [site.get("strict_url") for site in statistic_sites + if site.get("name") in sites] + + raw_statistics = self.dbhelper.get_site_user_statistics(strict_urls=site_urls) + if encoding == "RAW": + return raw_statistics + + return self.__todict(raw_statistics) + + def get_pt_site_activity_history(self, site, days=365 * 2): + """ + 查询站点 上传,下载,做种数据 + :param site: 站点名称 + :param days: 最大数据量 + :return: + """ + site_activities = [["time", "upload", "download", "bonus", "seeding", "seeding_size"]] + sql_site_activities = self.dbhelper.get_site_statistics_history(site=site, days=days) + for sql_site_activity in sql_site_activities: + timestamp = datetime.strptime(sql_site_activity.DATE, '%Y-%m-%d').timestamp() * 1000 + site_activities.append( + [timestamp, + sql_site_activity.UPLOAD, + sql_site_activity.DOWNLOAD, + sql_site_activity.BONUS, + sql_site_activity.SEEDING, + sql_site_activity.SEEDING_SIZE]) + + return site_activities + + def get_pt_site_seeding_info(self, site): + """ + 查询站点 做种分布信息 + :param site: 站点名称 + :return: seeding_info:[uploader_num, seeding_size] + """ + site_seeding_info = {"seeding_info": []} + seeding_info = self.dbhelper.get_site_seeding_info(site=site) + if not seeding_info: + return site_seeding_info + + site_seeding_info["seeding_info"] = json.loads(seeding_info[0]) + return site_seeding_info + + @staticmethod + def __todict(raw_statistics): + statistics = [] + for site in raw_statistics: + statistics.append({"site": site.SITE, + "username": site.USERNAME, + "user_level": site.USER_LEVEL, + "join_at": site.JOIN_AT, + "update_at": site.UPDATE_AT, + "upload": site.UPLOAD, + "download": site.DOWNLOAD, + "ratio": site.RATIO, + "seeding": site.SEEDING, + "leeching": site.LEECHING, + "seeding_size": site.SEEDING_SIZE, + "bonus": site.BONUS, + "url": site.URL, + "msg_unread": site.MSG_UNREAD + }) + return statistics diff --git a/app/sites/sites.py b/app/sites/sites.py new file mode 100644 index 0000000..676d134 --- /dev/null +++ b/app/sites/sites.py @@ -0,0 +1,422 @@ +import json +import random +import time +from datetime import datetime +from functools import lru_cache + +from lxml import etree + +from app.conf import SiteConf +from app.helper import ChromeHelper, SiteHelper, DbHelper +from app.message import Message +from app.utils import RequestUtils, StringUtils, ExceptionUtils +from app.utils.commons import singleton +from config import Config + + +@singleton +class Sites: + message = None + dbhelper = None + + _sites = [] + _siteByIds = {} + _siteByUrls = {} + _site_favicons = {} + _rss_sites = [] + _brush_sites = [] + _statistic_sites = [] + _signin_sites = [] + + _MAX_CONCURRENCY = 10 + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.message = Message() + # 原始站点列表 + self._sites = [] + # ID存储站点 + self._siteByIds = {} + # URL存储站点 + self._siteByUrls = {} + # 开启订阅功能站点 + self._rss_sites = [] + # 开启刷流功能站点: + self._brush_sites = [] + # 开启统计功能站点: + self._statistic_sites = [] + # 开启签到功能站点: + self._signin_sites = [] + # 站点图标 + self.init_favicons() + # 站点数据 + self._sites = self.dbhelper.get_config_site() + for site in self._sites: + # 站点属性 + site_note = self.__get_site_note_items(site.NOTE) + # 站点用途:Q签到、D订阅、S刷流 + site_rssurl = site.RSSURL + site_signurl = site.SIGNURL + site_cookie = site.COOKIE + site_uses = site.INCLUDE or '' + uses = [] + if site_uses: + signin_enable = True if "Q" in site_uses and site_signurl and site_cookie else False + rss_enable = True if "D" in site_uses and site_rssurl else False + brush_enable = True if "S" in site_uses and site_rssurl and site_cookie else False + statistic_enable = True if "T" in site_uses and (site_rssurl or site_signurl) and site_cookie else False + uses.append("Q") if signin_enable else None + uses.append("D") if rss_enable else None + uses.append("S") if brush_enable else None + uses.append("T") if statistic_enable else None + else: + signin_enable = False + rss_enable = False + brush_enable = False + statistic_enable = False + site_info = { + "id": site.ID, + "name": site.NAME, + "pri": site.PRI or 0, + "rssurl": site_rssurl, + "signurl": site_signurl, + "cookie": site_cookie, + "rule": site_note.get("rule"), + "download_setting": site_note.get("download_setting"), + "signin_enable": signin_enable, + "rss_enable": rss_enable, + "brush_enable": brush_enable, + "statistic_enable": statistic_enable, + "uses": uses, + "ua": site_note.get("ua"), + "parse": True if site_note.get("parse") == "Y" else False, + "unread_msg_notify": True if site_note.get("message") == "Y" else False, + "chrome": True if site_note.get("chrome") == "Y" else False, + "proxy": True if site_note.get("proxy") == "Y" else False, + "subtitle": True if site_note.get("subtitle") == "Y" else False, + "strict_url": StringUtils.get_base_url(site_signurl or site_rssurl) + } + # 以ID存储 + self._siteByIds[site.ID] = site_info + # 以域名存储 + site_strict_url = StringUtils.get_url_domain(site.SIGNURL or site.RSSURL) + if site_strict_url: + self._siteByUrls[site_strict_url] = site_info + + def init_favicons(self): + """ + 加载图标到内存 + """ + self._site_favicons = {site.SITE: site.FAVICON for site in self.dbhelper.get_site_favicons()} + + def get_sites(self, + siteid=None, + siteurl=None, + rss=False, + brush=False, + signin=False, + statistic=False): + """ + 获取站点配置 + """ + if siteid: + return self._siteByIds.get(int(siteid)) or {} + if siteurl: + return self._siteByUrls.get(StringUtils.get_url_domain(siteurl)) or {} + + ret_sites = [] + for site in self._siteByIds.values(): + if rss and not site.get('rss_enable'): + continue + if brush and not site.get('brush_enable'): + continue + if signin and not site.get('signin_enable'): + continue + if statistic and not site.get('statistic_enable'): + continue + ret_sites.append(site) + if siteid or siteurl: + return {} + return ret_sites + + def get_site_dict(self, + rss=False, + brush=False, + signin=False, + statistic=False): + """ + 获取站点字典 + """ + return [ + { + "id": site.get("id"), + "name": site.get("name") + } for site in self.get_sites( + rss=rss, + brush=brush, + signin=signin, + statistic=statistic + ) + ] + + def get_site_names(self, + rss=False, + brush=False, + signin=False, + statistic=False): + """ + 获取站点名称 + """ + return [ + site.get("name") for site in self.get_sites( + rss=rss, + brush=brush, + signin=signin, + statistic=statistic + ) + ] + + def get_site_favicon(self, site_name=None): + """ + 获取站点图标 + """ + if site_name: + return self._site_favicons.get(site_name) + else: + return self._site_favicons + + def get_site_download_setting(self, site_name=None): + """ + 获取站点下载设置 + """ + if site_name: + for site in self._siteByIds.values(): + if site.get("name") == site_name: + return site.get("download_setting") + return None + + def test_connection(self, site_id): + """ + 测试站点连通性 + :param site_id: 站点编号 + :return: 是否连通、错误信息、耗时 + """ + site_info = self.get_sites(siteid=site_id) + if not site_info: + return False, "站点不存在", 0 + site_cookie = site_info.get("cookie") + if not site_cookie: + return False, "未配置站点Cookie", 0 + ua = site_info.get("ua") + site_url = StringUtils.get_base_url(site_info.get("signurl") or site_info.get("rssurl")) + if not site_url: + return False, "未配置站点地址", 0 + chrome = ChromeHelper() + if site_info.get("chrome") and chrome.get_status(): + # 计时 + start_time = datetime.now() + if not chrome.visit(url=site_url, ua=ua, cookie=site_cookie): + return False, "Chrome模拟访问失败", 0 + # 循环检测是否过cf + cloudflare = chrome.pass_cloudflare() + seconds = int((datetime.now() - start_time).microseconds / 1000) + if not cloudflare: + return False, "跳转站点失败", seconds + # 判断是否已签到 + html_text = chrome.get_html() + if not html_text: + return False, "获取站点源码失败", 0 + if SiteHelper.is_logged_in(html_text): + return True, "连接成功", seconds + else: + return False, "Cookie失效", seconds + else: + # 计时 + start_time = datetime.now() + res = RequestUtils(cookies=site_cookie, + headers=ua, + proxies=Config().get_proxies() if site_info.get("proxy") else None + ).get_res(url=site_url) + seconds = int((datetime.now() - start_time).microseconds / 1000) + if res and res.status_code == 200: + if not SiteHelper.is_logged_in(res.text): + return False, "Cookie失效", seconds + else: + return True, "连接成功", seconds + elif res is not None: + return False, f"连接失败,状态码:{res.status_code}", seconds + else: + return False, "无法打开网站", seconds + + def get_site_attr(self, url): + """ + 整合公有站点和私有站点的属性 + """ + site_info = self.get_sites(siteurl=url) + public_site = self.get_public_sites(url=url) + if public_site: + site_info.update(public_site) + return site_info + + def parse_site_download_url(self, page_url, xpath): + """ + 从站点详情页面中解析中下载链接 + :param page_url: 详情页面地址 + :param xpath: 解析XPATH,同时还包括Cookie、UA和Referer + """ + if not page_url or not xpath: + return "" + cookie, ua, referer, page_source = None, None, None, None + xpaths = xpath.split("|") + xpath = xpaths[0] + if len(xpaths) > 1: + cookie = xpaths[1] + if len(xpaths) > 2: + ua = xpaths[2] + if len(xpaths) > 3: + referer = xpaths[3] + try: + site_info = self.get_public_sites(url=page_url) + if not site_info.get("referer"): + referer = None + req = RequestUtils( + headers=ua, + cookies=cookie, + referer=referer, + proxies=Config().get_proxies() if site_info.get("proxy") else None + ).get_res(url=page_url) + if req and req.status_code == 200: + if req.text: + page_source = req.text + # xpath解析 + if page_source: + html = etree.HTML(page_source) + urls = html.xpath(xpath) + if urls: + return str(urls[0]) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return None + + @staticmethod + @lru_cache(maxsize=128) + def __get_site_page_html(url, cookie, ua, render=False, proxy=False): + chrome = ChromeHelper(headless=True) + if render and chrome.get_status(): + # 开渲染 + if chrome.visit(url=url, cookie=cookie, ua=ua): + # 等待页面加载完成 + time.sleep(10) + return chrome.get_html() + else: + res = RequestUtils( + cookies=cookie, + headers=ua, + proxies=Config().get_proxies() if proxy else None + ).get_res(url=url) + if res and res.status_code == 200: + res.encoding = res.apparent_encoding + return res.text + return "" + + @staticmethod + def get_grapsite_conf(url): + """ + 根据地址找到RSS_SITE_GRAP_CONF对应配置 + """ + for k, v in SiteConf.RSS_SITE_GRAP_CONF.items(): + if StringUtils.url_equal(k, url): + return v + return {} + + def check_torrent_attr(self, torrent_url, cookie, ua=None, proxy=False): + """ + 检验种子是否免费,当前做种人数 + :param torrent_url: 种子的详情页面 + :param cookie: 站点的Cookie + :param ua: 站点的ua + :param proxy: 是否使用代理 + :return: 种子属性,包含FREE 2XFREE HR PEER_COUNT等属性 + """ + ret_attr = { + "free": False, + "2xfree": False, + "hr": False, + "peer_count": 0 + } + if not torrent_url: + return ret_attr + xpath_strs = self.get_grapsite_conf(torrent_url) + if not xpath_strs: + return ret_attr + html_text = self.__get_site_page_html(url=torrent_url, + cookie=cookie, + ua=ua, + render=xpath_strs.get('RENDER'), + proxy=proxy) + if not html_text: + return ret_attr + try: + html = etree.HTML(html_text) + # 检测2XFREE + for xpath_str in xpath_strs.get("2XFREE"): + if html.xpath(xpath_str): + ret_attr["free"] = True + ret_attr["2xfree"] = True + # 检测FREE + for xpath_str in xpath_strs.get("FREE"): + if html.xpath(xpath_str): + ret_attr["free"] = True + # 检测HR + for xpath_str in xpath_strs.get("HR"): + if html.xpath(xpath_str): + ret_attr["hr"] = True + # 检测PEER_COUNT当前做种人数 + for xpath_str in xpath_strs.get("PEER_COUNT"): + peer_count_dom = html.xpath(xpath_str) + if peer_count_dom: + peer_count_str = ''.join(peer_count_dom[0].itertext()) + peer_count_digit_str = "" + for m in peer_count_str: + if m.isdigit(): + peer_count_digit_str = peer_count_digit_str + m + ret_attr["peer_count"] = int(peer_count_digit_str) if len(peer_count_digit_str) > 0 else 0 + except Exception as err: + ExceptionUtils.exception_traceback(err) + # 随机休眼后再返回 + time.sleep(round(random.uniform(1, 5), 1)) + return ret_attr + + @staticmethod + def is_public_site(url): + """ + 判断是否为公开BT站点 + """ + _, netloc = StringUtils.get_url_netloc(url) + if netloc in SiteConf.PUBLIC_TORRENT_SITES.keys(): + return True + return False + + @staticmethod + def get_public_sites(url=None): + """ + 查询所有公开BT站点 + """ + if url: + _, netloc = StringUtils.get_url_netloc(url) + return SiteConf.PUBLIC_TORRENT_SITES.get(netloc) or {} + else: + return SiteConf.PUBLIC_TORRENT_SITES.items() + + @staticmethod + def __get_site_note_items(note): + """ + 从note中提取站点信息 + """ + infos = {} + if note: + infos = json.loads(note) + return infos diff --git a/app/sites/sitesignin/_base.py b/app/sites/sitesignin/_base.py new file mode 100644 index 0000000..30c4eaf --- /dev/null +++ b/app/sites/sitesignin/_base.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from abc import ABCMeta, abstractmethod + +from app.utils import StringUtils + + +class _ISiteSigninHandler(metaclass=ABCMeta): + """ + 实现站点签到的基类,所有站点签到类都需要继承此类,并实现match和signin方法 + 实现类放置到sitesignin目录下将会自动加载 + """ + # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url + site_url = "" + + @abstractmethod + def match(self, url): + """ + 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可 + :param url: 站点Url + :return: 是否匹配,如匹配则会调用该类的signin方法 + """ + return True if StringUtils.url_equal(url, self.site_url) else False + + @abstractmethod + def signin(self, site_info: dict): + """ + 执行签到操作 + :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息 + :return: 签到结果信息 + """ + pass diff --git a/app/sites/siteuserinfo/__init__.py b/app/sites/siteuserinfo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/sites/siteuserinfo/_base.py b/app/sites/siteuserinfo/_base.py new file mode 100644 index 0000000..fc5695a --- /dev/null +++ b/app/sites/siteuserinfo/_base.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +import base64 +import json +import re +from abc import ABCMeta, abstractmethod +from urllib.parse import urljoin, urlsplit + +import requests +from lxml import etree + +import log +from app.helper import SiteHelper +from app.utils import RequestUtils +from app.utils.types import SiteSchema + +SITE_BASE_ORDER = 1000 + + +class _ISiteUserInfo(metaclass=ABCMeta): + # 站点模版 + schema = SiteSchema.NexusPhp + # 站点解析时判断顺序,值越小越先解析 + order = SITE_BASE_ORDER + + def __init__(self, site_name, url, site_cookie, index_html, session=None, ua=None): + super().__init__() + # 站点信息 + self.site_name = None + self.site_url = None + self.site_favicon = None + # 用户信息 + self.username = None + self.userid = None + # 未读消息 + self.message_unread = 0 + self.message_unread_contents = [] + + # 流量信息 + self.upload = 0 + self.download = 0 + self.ratio = 0 + + # 种子信息 + self.seeding = 0 + self.leeching = 0 + self.uploaded = 0 + self.completed = 0 + self.incomplete = 0 + self.seeding_size = 0 + self.leeching_size = 0 + self.uploaded_size = 0 + self.completed_size = 0 + self.incomplete_size = 0 + # 做种人数, 种子大小 + self.seeding_info = [] + + # 用户详细信息 + self.user_level = None + self.join_at = None + self.bonus = 0.0 + + # 错误信息 + self.err_msg = None + # 内部数据 + self._base_url = None + self._site_cookie = None + self._index_html = None + self._addition_headers = None + + # 站点页面 + self._brief_page = "index.php" + self._user_detail_page = "userdetails.php?id=" + self._user_traffic_page = "index.php" + self._torrent_seeding_page = "getusertorrentlistajax.php?userid=" + self._user_mail_unread_page = "messages.php?action=viewmailbox&box=1&unread=yes" + self._sys_mail_unread_page = "messages.php?action=viewmailbox&box=-2&unread=yes" + self._torrent_seeding_params = None + self._torrent_seeding_headers = None + + split_url = urlsplit(url) + self.site_name = site_name + self.site_url = url + self._base_url = f"{split_url.scheme}://{split_url.netloc}" + self._favicon_url = urljoin(self._base_url, "favicon.ico") + self.site_favicon = "" + self._site_cookie = site_cookie + self._index_html = index_html + self._session = session if session else requests.Session() + self._ua = ua + + def site_schema(self): + """ + 站点解析模型 + :return: 站点解析模型 + """ + return self.schema + + @classmethod + def match(cls, html_text): + """ + 是否匹配当前解析模型 + :param html_text: 站点首页html + :return: 是否匹配 + """ + return False + + def parse(self): + """ + 解析站点信息 + :return: + """ + self._parse_favicon(self._index_html) + if not self._parse_logged_in(self._index_html): + return + + self._parse_site_page(self._index_html) + self._parse_user_base_info(self._index_html) + self._pase_unread_msgs() + if self._user_traffic_page: + self._parse_user_traffic_info(self._get_page_content(urljoin(self._base_url, self._user_traffic_page))) + if self._user_detail_page: + self._parse_user_detail_info(self._get_page_content(urljoin(self._base_url, self._user_detail_page))) + + self._parse_seeding_pages() + self.seeding_info = json.dumps(self.seeding_info) + + def _pase_unread_msgs(self): + """ + 解析所有未读消息标题和内容 + :return: + """ + unread_msg_links = [] + if self.message_unread > 0: + links = {self._user_mail_unread_page, self._sys_mail_unread_page} + for link in links: + if not link: + continue + + msg_links = [] + next_page = self._parse_message_unread_links( + self._get_page_content(urljoin(self._base_url, link)), msg_links) + while next_page: + next_page = self._parse_message_unread_links( + self._get_page_content(urljoin(self._base_url, next_page)), msg_links) + + unread_msg_links.extend(msg_links) + + for msg_link in unread_msg_links: + print(msg_link) + log.debug(f"【Sites】{self.site_name} 信息链接 {msg_link}") + head, date, content = self._parse_message_content(self._get_page_content(urljoin(self._base_url, msg_link))) + log.debug(f"【Sites】{self.site_name} 标题 {head} 时间 {date} 内容 {content}") + self.message_unread_contents.append((head, date, content)) + + def _parse_seeding_pages(self): + seeding_pages = [] + if self._torrent_seeding_page: + if isinstance(self._torrent_seeding_page, list): + seeding_pages.extend(self._torrent_seeding_page) + else: + seeding_pages.append(self._torrent_seeding_page) + + for seeding_page in seeding_pages: + # 第一页 + next_page = self._parse_user_torrent_seeding_info( + self._get_page_content(urljoin(self._base_url, seeding_page), + self._torrent_seeding_params, + self._torrent_seeding_headers)) + + # 其他页处理 + while next_page: + next_page = self._parse_user_torrent_seeding_info( + self._get_page_content(urljoin(urljoin(self._base_url, seeding_page), next_page), + self._torrent_seeding_params, + self._torrent_seeding_headers), + multi_page=True) + + @staticmethod + def _prepare_html_text(html_text): + """ + 处理掉HTML中的干扰部分 + """ + return re.sub(r"#\d+", "", re.sub(r"\d+px", "", html_text)) + + @abstractmethod + def _parse_message_unread_links(self, html_text, msg_links): + """ + 获取未阅读消息链接 + :param html_text: + :return: + """ + pass + + def _parse_favicon(self, html_text): + """ + 解析站点favicon,返回base64 fav图标 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if html: + fav_link = html.xpath('//head/link[contains(@rel, "icon")]/@href') + if fav_link: + self._favicon_url = urljoin(self._base_url, fav_link[0]) + + res = RequestUtils(cookies=self._site_cookie, session=self._session, timeout=60, headers=self._ua).get_res( + url=self._favicon_url) + if res: + self.site_favicon = base64.b64encode(res.content).decode() + + def _get_page_content(self, url, params=None, headers=None): + """ + :param url: 网页地址 + :param params: post参数 + :param headers: 额外的请求头 + :return: + """ + req_headers = None + if self._ua or headers or self._addition_headers: + req_headers = {} + if headers: + req_headers.update(headers) + + if isinstance(self._ua, str): + req_headers.update({ + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "User-Agent": f"{self._ua}" + }) + else: + req_headers.update(self._ua) + + if self._addition_headers: + req_headers.update(self._addition_headers) + + if params: + res = RequestUtils(cookies=self._site_cookie, session=self._session, timeout=60, + headers=req_headers).post_res( + url=url, params=params) + else: + res = RequestUtils(cookies=self._site_cookie, session=self._session, timeout=60, + headers=req_headers).get_res( + url=url) + if res is not None and res.status_code in (200, 500): + if "charset=utf-8" in res.text or "charset=UTF-8" in res.text: + res.encoding = "UTF-8" + else: + res.encoding = res.apparent_encoding + return res.text + + return "" + + @abstractmethod + def _parse_site_page(self, html_text): + """ + 解析站点相关信息页面 + :param html_text: + :return: + """ + pass + + @abstractmethod + def _parse_user_base_info(self, html_text): + """ + 解析用户基础信息 + :param html_text: + :return: + """ + pass + + def _parse_logged_in(self, html_text): + """ + 解析用户是否已经登陆 + :param html_text: + :return: True/False + """ + logged_in = SiteHelper.is_logged_in(html_text) + if not logged_in: + self.err_msg = "未检测到已登陆,请检查cookies是否过期" + log.warn(f"【Sites】{self.site_name} 未登录,跳过后续操作") + + return logged_in + + @abstractmethod + def _parse_user_traffic_info(self, html_text): + """ + 解析用户的上传,下载,分享率等信息 + :param html_text: + :return: + """ + pass + + @abstractmethod + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 解析用户的做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + pass + + @abstractmethod + def _parse_user_detail_info(self, html_text): + """ + 解析用户的详细信息 + 加入时间/等级/魔力值等 + :param html_text: + :return: + """ + pass + + @abstractmethod + def _parse_message_content(self, html_text): + """ + 解析短消息内容 + :param html_text: + :return: head: message, date: time, content: message content + """ + pass diff --git a/app/sites/siteuserinfo/discuz.py b/app/sites/siteuserinfo/discuz.py new file mode 100644 index 0000000..41717a1 --- /dev/null +++ b/app/sites/siteuserinfo/discuz.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class DiscuzUserInfo(_ISiteUserInfo): + schema = SiteSchema.DiscuzX + order = SITE_BASE_ORDER + 10 + + @classmethod + def match(cls, html_text): + html = etree.HTML(html_text) + if not html: + return False + + printable_text = html.xpath("string(.)") if html else "" + return 'Powered by Discuz!' in printable_text + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + + user_info = html.xpath('//a[contains(@href, "&uid=")]') + if user_info: + user_id_match = re.search(r"&uid=(\d+)", user_info[0].attrib['href']) + if user_id_match and user_id_match.group().strip(): + self.userid = user_id_match.group(1) + self._torrent_seeding_page = f"forum.php?&mod=torrents&cat_5up=on" + self._user_detail_page = user_info[0].attrib['href'] + self.username = user_info[0].text.strip() + + def _parse_site_page(self, html_text): + # TODO + pass + + def _parse_user_detail_info(self, html_text): + """ + 解析用户额外信息,加入时间,等级 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return None + + # 用户等级 + user_levels_text = html.xpath('//a[contains(@href, "usergroup")]/text()') + if user_levels_text: + self.user_level = user_levels_text[-1].strip() + + # 加入日期 + join_at_text = html.xpath('//li[em[text()="注册时间"]]/text()') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str(join_at_text[0].strip()) + + # 分享率 + ratio_text = html.xpath('//li[contains(.//text(), "分享率")]//text()') + if ratio_text: + ratio_match = re.search(r"\(([\d,.]+)\)", ratio_text[0]) + if ratio_match and ratio_match.group(1).strip(): + self.bonus = StringUtils.str_float(ratio_match.group(1)) + + # 积分 + bouns_text = html.xpath('//li[em[text()="积分"]]/text()') + if bouns_text: + self.bonus = StringUtils.str_float(bouns_text[0].strip()) + + # 上传 + upload_text = html.xpath('//li[em[contains(text(),"上传量")]]/text()') + if upload_text: + self.upload = StringUtils.num_filesize(upload_text[0].strip().split('/')[-1]) + + # 下载 + download_text = html.xpath('//li[em[contains(text(),"下载量")]]/text()') + if download_text: + self.download = StringUtils.num_filesize(download_text[0].strip().split('/')[-1]) + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(html_text) + if not html: + return None + + size_col = 3 + seeders_col = 4 + # 搜索size列 + if html.xpath('//tr[position()=1]/td[.//img[@class="size"] and .//img[@alt="size"]]'): + size_col = len(html.xpath('//tr[position()=1]/td[.//img[@class="size"] ' + 'and .//img[@alt="size"]]/preceding-sibling::td')) + 1 + # 搜索seeders列 + if html.xpath('//tr[position()=1]/td[.//img[@class="seeders"] and .//img[@alt="seeders"]]'): + seeders_col = len(html.xpath('//tr[position()=1]/td[.//img[@class="seeders"] ' + 'and .//img[@alt="seeders"]]/preceding-sibling::td')) + 1 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//tr[position()>1]/td[{size_col}]') + seeding_seeders = html.xpath(f'//tr[position()>1]/td[{seeders_col}]//text()') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i]) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + next_page_text = html.xpath('//a[contains(.//text(), "下一页") or contains(.//text(), "下一頁")]/@href') + if next_page_text: + next_page = next_page_text[-1].strip() + + return next_page + + def _parse_user_traffic_info(self, html_text): + pass + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/file_list.py b/app/sites/siteuserinfo/file_list.py new file mode 100644 index 0000000..59d823b --- /dev/null +++ b/app/sites/siteuserinfo/file_list.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class FileListSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.FileList + order = SITE_BASE_ORDER + 50 + + @classmethod + def match(cls, html_text): + html = etree.HTML(html_text) + if not html: + return False + + printable_text = html.xpath("string(.)") if html else "" + return 'Powered by FileList' in printable_text + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + user_detail = re.search(r"userdetails.php\?id=(\d+)", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = user_detail.group(1) + + self._torrent_seeding_page = f"snatchlist.php?id={self.userid}&action=torrents&type=seeding" + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + + ret = html.xpath(f'//a[contains(@href, "userdetails") and contains(@href, "{self.userid}")]//text()') + if ret: + self.username = str(ret[0]) + + def _parse_user_traffic_info(self, html_text): + """ + 上传/下载/分享率 [做种数/魔力值] + :param html_text: + :return: + """ + return + + def _parse_user_detail_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + + upload_html = html.xpath('//table//tr/td[text()="Uploaded"]/following-sibling::td//text()') + if upload_html: + self.upload = StringUtils.num_filesize(upload_html[0]) + download_html = html.xpath('//table//tr/td[text()="Downloaded"]/following-sibling::td//text()') + if download_html: + self.download = StringUtils.num_filesize(download_html[0]) + + self.ratio = 0 if self.download == 0 else self.upload / self.download + + user_level_html = html.xpath('//table//tr/td[text()="Class"]/following-sibling::td//text()') + if user_level_html: + self.user_level = user_level_html[0].strip() + + join_at_html = html.xpath('//table//tr/td[contains(text(), "Join")]/following-sibling::td//text()') + if join_at_html: + self.join_at = StringUtils.unify_datetime_str(join_at_html[0].strip()) + + bonus_html = html.xpath('//a[contains(@href, "shop.php")]') + if bonus_html: + self.bonus = StringUtils.str_float(bonus_html[0].xpath("string(.)").strip()) + pass + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(html_text) + if not html: + return None + + size_col = 6 + seeders_col = 7 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//table/tr[position()>1]/td[{size_col}]') + seeding_seeders = html.xpath(f'//table/tr[position()>1]/td[{seeders_col}]') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i].xpath("string(.)").strip()) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + + return next_page + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/gazelle.py b/app/sites/siteuserinfo/gazelle.py new file mode 100644 index 0000000..ec4dfe4 --- /dev/null +++ b/app/sites/siteuserinfo/gazelle.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class GazelleSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.Gazelle + order = SITE_BASE_ORDER + + @classmethod + def match(cls, html_text): + html = etree.HTML(html_text) + if not html: + return False + + printable_text = html.xpath("string(.)") if html else "" + + return "Powered by Gazelle" in printable_text or "DIC Music" in printable_text + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + + tmps = html.xpath('//a[contains(@href, "user.php?id=")]') + if tmps: + user_id_match = re.search(r"user.php\?id=(\d+)", tmps[0].attrib['href']) + if user_id_match and user_id_match.group().strip(): + self.userid = user_id_match.group(1) + self._torrent_seeding_page = f"torrents.php?type=seeding&userid={self.userid}" + self._user_detail_page = f"user.php?id={self.userid}" + self.username = tmps[0].text.strip() + + tmps = html.xpath('//*[@id="header-uploaded-value"]/@data-value') + if tmps: + self.upload = StringUtils.num_filesize(tmps[0]) + else: + tmps = html.xpath('//li[@id="stats_seeding"]/span/text()') + if tmps: + self.upload = StringUtils.num_filesize(tmps[0]) + + tmps = html.xpath('//*[@id="header-downloaded-value"]/@data-value') + if tmps: + self.download = StringUtils.num_filesize(tmps[0]) + else: + tmps = html.xpath('//li[@id="stats_leeching"]/span/text()') + if tmps: + self.download = StringUtils.num_filesize(tmps[0]) + + self.ratio = 0.0 if self.download <= 0.0 else round(self.upload / self.download, 3) + + tmps = html.xpath('//a[contains(@href, "bonus.php")]/@data-tooltip') + if tmps: + bonus_match = re.search(r"([\d,.]+)", tmps[0]) + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + else: + tmps = html.xpath('//a[contains(@href, "bonus.php")]') + if tmps: + bonus_text = tmps[0].xpath("string(.)") + bonus_match = re.search(r"([\d,.]+)", bonus_text) + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + + def _parse_site_page(self, html_text): + # TODO + pass + + def _parse_user_detail_info(self, html_text): + """ + 解析用户额外信息,加入时间,等级 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return None + + # 用户等级 + user_levels_text = html.xpath('//*[@id="class-value"]/@data-value') + if user_levels_text: + self.user_level = user_levels_text[0].strip() + else: + user_levels_text = html.xpath('//li[contains(text(), "用户等级")]/text()') + if user_levels_text: + self.user_level = user_levels_text[0].split(':')[1].strip() + + # 加入日期 + join_at_text = html.xpath('//*[@id="join-date-value"]/@data-value') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str(join_at_text[0].strip()) + else: + join_at_text = html.xpath( + '//div[contains(@class, "box_userinfo_stats")]//li[contains(text(), "加入时间")]/span/text()') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str(join_at_text[0].strip()) + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(html_text) + if not html: + return None + + size_col = 3 + # 搜索size列 + if html.xpath('//table[contains(@id, "torrent")]//tr[1]/td'): + size_col = len(html.xpath('//table[contains(@id, "torrent")]//tr[1]/td')) - 3 + # 搜索seeders列 + seeders_col = size_col + 2 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{size_col}]') + seeding_seeders = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{seeders_col}]/text()') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = int(seeding_seeders[i]) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + if multi_page: + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + else: + if not self.seeding: + self.seeding = page_seeding + if not self.seeding_size: + self.seeding_size = page_seeding_size + if not self.seeding_info: + self.seeding_info = page_seeding_info + + # 是否存在下页数据 + next_page = None + next_page_text = html.xpath('//a[contains(.//text(), "Next") or contains(.//text(), "下一页")]/@href') + if next_page_text: + next_page = next_page_text[-1].strip() + + return next_page + + def _parse_user_traffic_info(self, html_text): + # TODO + pass + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/ipt_project.py b/app/sites/siteuserinfo/ipt_project.py new file mode 100644 index 0000000..cb57e10 --- /dev/null +++ b/app/sites/siteuserinfo/ipt_project.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class IptSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.Ipt + order = SITE_BASE_ORDER + 35 + + @classmethod + def match(cls, html_text): + return 'IPTorrents' in html_text + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + tmps = html.xpath('//a[contains(@href, "/u/")]//text()') + tmps_id = html.xpath('//a[contains(@href, "/u/")]/@href') + if tmps: + self.username = str(tmps[-1]) + if tmps_id: + user_id_match = re.search(r"/u/(\d+)", tmps_id[0]) + if user_id_match and user_id_match.group().strip(): + self.userid = user_id_match.group(1) + self._user_detail_page = f"user.php?u={self.userid}" + self._torrent_seeding_page = f"peers?u={self.userid}" + + tmps = html.xpath('//div[@class = "stats"]/div/div') + if tmps: + self.upload = StringUtils.num_filesize(str(tmps[0].xpath('span/text()')[1]).strip()) + self.download = StringUtils.num_filesize(str(tmps[0].xpath('span/text()')[2]).strip()) + self.seeding = StringUtils.str_int(tmps[0].xpath('a')[2].xpath('text()')[0]) + self.leeching = StringUtils.str_int(tmps[0].xpath('a')[2].xpath('text()')[1]) + self.ratio = StringUtils.str_float(str(tmps[0].xpath('span/text()')[0]).strip().replace('-', '0')) + self.bonus = StringUtils.str_float(tmps[0].xpath('a')[3].xpath('text()')[0]) + + def _parse_site_page(self, html_text): + # TODO + pass + + def _parse_user_detail_info(self, html_text): + html = etree.HTML(html_text) + if not html: + return + + user_levels_text = html.xpath('//tr/th[text()="Class"]/following-sibling::td[1]/text()') + if user_levels_text: + self.user_level = user_levels_text[0].strip() + + # 加入日期 + join_at_text = html.xpath('//tr/th[text()="Join date"]/following-sibling::td[1]/text()') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str(join_at_text[0].split(' (')[0]) + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + html = etree.HTML(html_text) + if not html: + return + # seeding start + seeding_end_pos = 3 + if html.xpath('//tr/td[text() = "Leechers"]'): + seeding_end_pos = len(html.xpath('//tr/td[text() = "Leechers"]/../preceding-sibling::tr')) + 1 + seeding_end_pos = seeding_end_pos - 3 + + page_seeding = 0 + page_seeding_size = 0 + seeding_torrents = html.xpath('//tr/td[text() = "Seeders"]/../following-sibling::tr/td[position()=6]/text()') + if seeding_torrents: + page_seeding = seeding_end_pos + for per_size in seeding_torrents[:seeding_end_pos]: + if '(' in per_size and ')' in per_size: + per_size = per_size.split('(')[-1] + per_size = per_size.split(')')[0] + + page_seeding_size += StringUtils.num_filesize(per_size) + + self.seeding = page_seeding + self.seeding_size = page_seeding_size + + def _parse_user_traffic_info(self, html_text): + # TODO + pass + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/nexus_php.py b/app/sites/siteuserinfo/nexus_php.py new file mode 100644 index 0000000..5c54015 --- /dev/null +++ b/app/sites/siteuserinfo/nexus_php.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +import log +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.exception_utils import ExceptionUtils +from app.utils.types import SiteSchema + + +class NexusPhpSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.NexusPhp + order = SITE_BASE_ORDER * 2 + + @classmethod + def match(cls, html_text): + """ + 默认使用NexusPhp解析 + :param html_text: + :return: + """ + return True + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + user_detail = re.search(r"userdetails.php\?id=(\d+)", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = user_detail.group(1) + self._torrent_seeding_page = f"getusertorrentlistajax.php?userid={self.userid}&type=seeding" + else: + user_detail = re.search(r"(userdetails)", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = None + self._torrent_seeding_page = None + + def _parse_message_unread(self, html_text): + """ + 解析未读短消息数量 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return + + message_labels = html.xpath('//a[contains(@href, "messages.php")]/..') + if message_labels: + message_text = message_labels[0].xpath("string(.)") + + log.debug(f"【Sites】{self.site_name} 消息原始信息 {message_text}") + message_unread_match = re.findall(r"[^Date](信息箱\s*|\(|你有\xa0)(\d+)", message_text) + + if message_unread_match and len(message_unread_match[-1]) == 2: + self.message_unread = StringUtils.str_int(message_unread_match[-1][1]) + + def _parse_user_base_info(self, html_text): + # 合并解析,减少额外请求调用 + self.__parse_user_traffic_info(html_text) + self._user_traffic_page = None + + self._parse_message_unread(html_text) + + html = etree.HTML(html_text) + if not html: + return + + ret = html.xpath(f'//a[contains(@href, "userdetails") and contains(@href, "{self.userid}")]//b//text()') + if ret: + self.username = str(ret[0]) + return + ret = html.xpath(f'//a[contains(@href, "userdetails") and contains(@href, "{self.userid}")]//text()') + if ret: + self.username = str(ret[0]) + + ret = html.xpath('//a[contains(@href, "userdetails")]//strong//text()') + if ret: + self.username = str(ret[0]) + return + + def __parse_user_traffic_info(self, html_text): + html_text = self._prepare_html_text(html_text) + upload_match = re.search(r"[^总]上[传傳]量?[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+[KMGTPI]*B)", html_text, + re.IGNORECASE) + self.upload = StringUtils.num_filesize(upload_match.group(1).strip()) if upload_match else 0 + download_match = re.search(r"[^总子影力]下[载載]量?[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+[KMGTPI]*B)", html_text, + re.IGNORECASE) + self.download = StringUtils.num_filesize(download_match.group(1).strip()) if download_match else 0 + ratio_match = re.search(r"分享率[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+)", html_text) + self.ratio = StringUtils.str_float(ratio_match.group(1)) if ( + ratio_match and ratio_match.group(1).strip()) else 0.0 + leeching_match = re.search(r"(Torrents leeching|下载中)[\u4E00-\u9FA5\D\s]+(\d+)[\s\S]+<", html_text) + self.leeching = StringUtils.str_int(leeching_match.group(2)) if leeching_match and leeching_match.group( + 2).strip() else 0 + html = etree.HTML(html_text) + tmps = html.xpath('//span[@class = "ucoin-symbol ucoin-gold"]//text()') if html else None + if tmps: + self.bonus = StringUtils.str_float(str(tmps[-1])) + return + tmps = html.xpath('//a[contains(@href,"mybonus")]/text()') if html else None + if tmps: + bonus_text = str(tmps[0]).strip() + bonus_match = re.search(r"([\d,.]+)", bonus_text) + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + return + bonus_match = re.search(r"mybonus.[\[\]::<>/a-zA-Z_\-=\"'\s#;.(使用魔力值豆]+\s*([\d,.]+)[<()&\s]", html_text) + try: + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + return + bonus_match = re.search(r"[魔力值|\]][\[\]::<>/a-zA-Z_\-=\"'\s#;]+\s*([\d,.]+)[<()&\s]", html_text, + flags=re.S) + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + except Exception as err: + ExceptionUtils.exception_traceback(err) + + def _parse_user_traffic_info(self, html_text): + """ + 上传/下载/分享率 [做种数/魔力值] + :param html_text: + :return: + """ + pass + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(str(html_text).replace(r'\/', '/')) + if not html: + return None + + size_col = 3 + seeders_col = 4 + # 搜索size列 + size_col_xpath = '//tr[position()=1]/td[(img[@class="size"] and img[@alt="size"]) or (text() = "大小")]' + if html.xpath(size_col_xpath): + size_col = len(html.xpath(f'{size_col_xpath}/preceding-sibling::td')) + 1 + # 搜索seeders列 + seeders_col_xpath = '//tr[position()=1]/td[(img[@class="seeders"] and img[@alt="seeders"]) or (text() = "在做种")]' + if html.xpath(seeders_col_xpath): + seeders_col = len(html.xpath(f'{seeders_col_xpath}/preceding-sibling::td')) + 1 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//tr[position()>1]/td[{size_col}]') + seeding_seeders = html.xpath(f'//tr[position()>1]/td[{seeders_col}]//text()') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i]) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + next_page_text = html.xpath('//a[contains(.//text(), "下一页") or contains(.//text(), "下一頁")]/@href') + if next_page_text: + next_page = next_page_text[-1].strip() + # fix up page url + if self.userid not in next_page: + next_page = f'{next_page}&userid={self.userid}&type=seeding' + + return next_page + + def _parse_user_detail_info(self, html_text): + """ + 解析用户额外信息,加入时间,等级 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return + + self.__get_user_level(html) + + # 加入日期 + join_at_text = html.xpath( + '//tr/td[text()="加入日期" or text()="注册日期" or *[text()="加入日期"]]/following-sibling::td[1]//text()' + '|//div/b[text()="加入日期"]/../text()') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str(join_at_text[0].split(' (')[0].strip()) + + # 做种体积 & 做种数 + # seeding 页面获取不到的话,此处再获取一次 + seeding_sizes = html.xpath('//tr/td[text()="当前上传"]/following-sibling::td[1]//' + 'table[tr[1][td[4 and text()="尺寸"]]]//tr[position()>1]/td[4]') + seeding_seeders = html.xpath('//tr/td[text()="当前上传"]/following-sibling::td[1]//' + 'table[tr[1][td[5 and text()="做种者"]]]//tr[position()>1]/td[5]//text()') + tmp_seeding = len(seeding_sizes) + tmp_seeding_size = 0 + tmp_seeding_info = [] + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i]) + + tmp_seeding_size += size + tmp_seeding_info.append([seeders, size]) + + if not self.seeding_size: + self.seeding_size = tmp_seeding_size + if not self.seeding: + self.seeding = tmp_seeding + if not self.seeding_info: + self.seeding_info = tmp_seeding_info + + seeding_sizes = html.xpath('//tr/td[text()="做种统计"]/following-sibling::td[1]//text()') + if seeding_sizes: + seeding_match = re.search(r"总做种数:\s+(\d+)", seeding_sizes[0], re.IGNORECASE) + seeding_size_match = re.search(r"总做种体积:\s+([\d,.\s]+[KMGTPI]*B)", seeding_sizes[0], re.IGNORECASE) + tmp_seeding = StringUtils.str_int(seeding_match.group(1)) if ( + seeding_match and seeding_match.group(1)) else 0 + tmp_seeding_size = StringUtils.num_filesize( + seeding_size_match.group(1).strip()) if seeding_size_match else 0 + if not self.seeding_size: + self.seeding_size = tmp_seeding_size + if not self.seeding: + self.seeding = tmp_seeding + + self.__fixup_torrent_seeding_page(html) + + def __fixup_torrent_seeding_page(self, html): + """ + 修正种子页面链接 + :param html: + :return: + """ + # 单独的种子页面 + seeding_url_text = html.xpath('//a[contains(@href,"getusertorrentlist.php") ' + 'and contains(@href,"seeding")]/@href') + if seeding_url_text: + self._torrent_seeding_page = seeding_url_text[0].strip() + # 从JS调用种获取用户ID + seeding_url_text = html.xpath('//a[contains(@href, "javascript: getusertorrentlistajax") ' + 'and contains(@href,"seeding")]/@href') + csrf_text = html.xpath('//meta[@name="x-csrf"]/@content') + if not self._torrent_seeding_page and seeding_url_text: + user_js = re.search(r"javascript: getusertorrentlistajax\(\s*'(\d+)", seeding_url_text[0]) + if user_js and user_js.group(1).strip(): + self.userid = user_js.group(1).strip() + self._torrent_seeding_page = f"getusertorrentlistajax.php?userid={self.userid}&type=seeding" + elif seeding_url_text and csrf_text: + if csrf_text[0].strip(): + self._torrent_seeding_page \ + = f"ajax_getusertorrentlist.php" + self._torrent_seeding_params = {'userid': self.userid, 'type': 'seeding', 'csrf': csrf_text[0].strip()} + + # 分类做种模式 + # 临时屏蔽 + # seeding_url_text = html.xpath('//tr/td[text()="当前做种"]/following-sibling::td[1]' + # '/table//td/a[contains(@href,"seeding")]/@href') + # if seeding_url_text: + # self._torrent_seeding_page = seeding_url_text + + def __get_user_level(self, html): + # 等级 获取同一行等级数据,图片格式等级,取title信息,否则取文本信息 + user_levels_text = html.xpath('//tr/td[text()="等級" or text()="等级" or *[text()="等级"]]/' + 'following-sibling::td[1]/img[1]/@title') + if user_levels_text: + self.user_level = user_levels_text[0].strip() + return + + user_levels_text = html.xpath('//tr/td[text()="等級" or text()="等级"]/' + 'following-sibling::td[1 and not(img)]' + '|//tr/td[text()="等級" or text()="等级"]/' + 'following-sibling::td[1 and img[not(@title)]]') + if user_levels_text: + self.user_level = user_levels_text[0].xpath("string(.)").strip() + return + + user_levels_text = html.xpath('//tr/td[text()="等級" or text()="等级"]/' + 'following-sibling::td[1]') + if user_levels_text: + self.user_level = user_levels_text[0].xpath("string(.)").strip() + return + + user_levels_text = html.xpath('//a[contains(@href, "userdetails")]/text()') + if not self.user_level and user_levels_text: + for user_level_text in user_levels_text: + user_level_match = re.search(r"\[(.*)]", user_level_text) + if user_level_match and user_level_match.group(1).strip(): + self.user_level = user_level_match.group(1).strip() + break + + def _parse_message_unread_links(self, html_text, msg_links): + html = etree.HTML(html_text) + if not html: + return None + + message_links = html.xpath('//tr[not(./td/img[@alt="Read"])]/td/a[contains(@href, "viewmessage")]/@href') + msg_links.extend(message_links) + # 是否存在下页数据 + next_page = None + next_page_text = html.xpath('//a[contains(.//text(), "下一页") or contains(.//text(), "下一頁")]/@href') + if next_page_text: + next_page = next_page_text[-1].strip() + + return next_page + + def _parse_message_content(self, html_text): + html = etree.HTML(html_text) + if not html: + return None, None, None + # 标题 + message_head_text = None + message_head = html.xpath('//h1/text()' + '|//div[@class="layui-card-header"]/span[1]/text()') + if message_head: + message_head_text = message_head[-1].strip() + + # 消息时间 + message_date_text = None + message_date = html.xpath('//h1/following-sibling::table[.//tr/td[@class="colhead"]]//tr[2]/td[2]' + '|//div[@class="layui-card-header"]/span[2]/span[2]') + if message_date: + message_date_text = message_date[0].xpath("string(.)").strip() + + # 消息内容 + message_content_text = None + message_content = html.xpath('//h1/following-sibling::table[.//tr/td[@class="colhead"]]//tr[3]/td' + '|//div[contains(@class,"layui-card-body")]') + if message_content: + message_content_text = message_content[0].xpath("string(.)").strip() + + return message_head_text, message_date_text, message_content_text diff --git a/app/sites/siteuserinfo/nexus_project.py b/app/sites/siteuserinfo/nexus_project.py new file mode 100644 index 0000000..0880998 --- /dev/null +++ b/app/sites/siteuserinfo/nexus_project.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import re + +from app.sites.siteuserinfo._base import SITE_BASE_ORDER +from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo +from app.utils.types import SiteSchema + + +class NexusProjectSiteUserInfo(NexusPhpSiteUserInfo): + schema = SiteSchema.NexusProject + order = SITE_BASE_ORDER + 25 + + @classmethod + def match(cls, html_text): + return 'Nexus Project' in html_text + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + user_detail = re.search(r"userdetails.php\?id=(\d+)", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = user_detail.group(1) + + self._torrent_seeding_page = f"viewusertorrents.php?id={self.userid}&show=seeding" diff --git a/app/sites/siteuserinfo/nexus_rabbit.py b/app/sites/siteuserinfo/nexus_rabbit.py new file mode 100644 index 0000000..6f76430 --- /dev/null +++ b/app/sites/siteuserinfo/nexus_rabbit.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +import json + +from lxml import etree + +from app.sites.siteuserinfo._base import SITE_BASE_ORDER +from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo +from app.utils.exception_utils import ExceptionUtils +from app.utils.types import SiteSchema + + +class NexusRabbitSiteUserInfo(NexusPhpSiteUserInfo): + schema = SiteSchema.NexusRabbit + order = SITE_BASE_ORDER + 5 + + @classmethod + def match(cls, html_text): + html = etree.HTML(html_text) + if not html: + return False + + printable_text = html.xpath("string(.)") if html else "" + return 'Style by Rabbit' in printable_text + + def _parse_site_page(self, html_text): + super()._parse_site_page(html_text) + self._torrent_seeding_page = f"getusertorrentlistajax.php?page=1&limit=5000000&type=seeding&uid={self.userid}" + self._torrent_seeding_headers = {"Accept": "application/json, text/javascript, */*; q=0.01"} + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + + try: + torrents = json.loads(html_text).get('data') + except Exception as e: + ExceptionUtils.exception_traceback(e) + return + + page_seeding_size = 0 + page_seeding_info = [] + + page_seeding = len(torrents) + for torrent in torrents: + seeders = int(torrent.get('seeders', 0)) + size = int(torrent.get('size', 0)) + page_seeding_size += int(torrent.get('size', 0)) + + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) diff --git a/app/sites/siteuserinfo/small_horse.py b/app/sites/siteuserinfo/small_horse.py new file mode 100644 index 0000000..875c282 --- /dev/null +++ b/app/sites/siteuserinfo/small_horse.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class SmallHorseSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.SmallHorse + order = SITE_BASE_ORDER + 30 + + @classmethod + def match(cls, html_text): + return 'Small Horse' in html_text + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + user_detail = re.search(r"user.php\?id=(\d+)", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = user_detail.group(1) + self._user_traffic_page = f"user.php?id={self.userid}" + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + ret = html.xpath('//a[contains(@href, "user.php")]//text()') + if ret: + self.username = str(ret[0]) + + def _parse_user_traffic_info(self, html_text): + """ + 上传/下载/分享率 [做种数/魔力值] + :param html_text: + :return: + """ + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + tmps = html.xpath('//ul[@class = "stats nobullet"]') + if tmps: + if tmps[1].xpath("li") and tmps[1].xpath("li")[0].xpath("span//text()"): + self.join_at = StringUtils.unify_datetime_str(tmps[1].xpath("li")[0].xpath("span//text()")[0]) + self.upload = StringUtils.num_filesize(str(tmps[1].xpath("li")[2].xpath("text()")[0]).split(":")[1].strip()) + self.download = StringUtils.num_filesize( + str(tmps[1].xpath("li")[3].xpath("text()")[0]).split(":")[1].strip()) + if tmps[1].xpath("li")[4].xpath("span//text()"): + self.ratio = StringUtils.str_float(str(tmps[1].xpath("li")[4].xpath("span//text()")[0]).replace('∞', '0')) + else: + self.ratio = StringUtils.str_float(str(tmps[1].xpath("li")[5].xpath("text()")[0]).split(":")[1]) + self.bonus = StringUtils.str_float(str(tmps[1].xpath("li")[5].xpath("text()")[0]).split(":")[1]) + self.user_level = str(tmps[3].xpath("li")[0].xpath("text()")[0]).split(":")[1].strip() + self.seeding = StringUtils.str_int( + (tmps[4].xpath("li")[5].xpath("text()")[0]).split(":")[1].replace("[", "")) + self.leeching = StringUtils.str_int( + (tmps[4].xpath("li")[6].xpath("text()")[0]).split(":")[1].replace("[", "")) + + def _parse_user_detail_info(self, html_text): + pass + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + pass + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/tnode.py b/app/sites/siteuserinfo/tnode.py new file mode 100644 index 0000000..d6846f3 --- /dev/null +++ b/app/sites/siteuserinfo/tnode.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +import json +import re + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class TNodeSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.TNode + order = SITE_BASE_ORDER + 60 + + @classmethod + def match(cls, html_text): + return 'Powered By TNode' in html_text + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + # + csrf_token = re.search(r'', html_text) + if csrf_token: + self._addition_headers = {'X-CSRF-TOKEN': csrf_token.group(1)} + self._user_detail_page = "api/user/getMainInfo" + self._torrent_seeding_page = "api/user/listTorrentActivity?id=&type=seeding&page=1&size=20000" + + def _parse_logged_in(self, html_text): + """ + 判断是否登录成功, 通过判断是否存在用户信息 + 暂时跳过检测,待后续优化 + :param html_text: + :return: + """ + return True + + def _parse_user_base_info(self, html_text): + self.username = self.userid + + def _parse_user_traffic_info(self, html_text): + pass + + def _parse_user_detail_info(self, html_text): + detail = json.loads(html_text) + if detail.get("status") != 200: + return + + user_info = detail.get("data", {}) + self.userid = user_info.get("id") + self.username = user_info.get("username") + self.user_level = user_info.get("class", {}).get("name") + self.join_at = user_info.get("regTime", 0) + self.join_at = StringUtils.unify_datetime_str(str(self.join_at)) + + self.upload = user_info.get("upload") + self.download = user_info.get("download") + self.ratio = 0 if self.download <= 0 else round(self.upload / self.download, 3) + self.bonus = user_info.get("bonus") + + self.message_unread = user_info.get("unreadAdmin", 0) + user_info.get("unreadInbox", 0) + user_info.get( + "unreadSystem", 0) + pass + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 解析用户做种信息 + """ + seeding_info = json.loads(html_text) + if seeding_info.get("status") != 200: + return + + torrents = seeding_info.get("data", {}).get("torrents", []) + + page_seeding_size = 0 + page_seeding_info = [] + for torrent in torrents: + size = torrent.get("size", 0) + seeders = torrent.get("seeding", 0) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += len(torrents) + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + + return next_page + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + """ + 系统信息 api/message/listSystem?page=1&size=20 + 收件箱信息 api/message/listInbox?page=1&size=20 + 管理员信息 api/message/listAdmin?page=1&size=20 + :param html_text: + :return: + """ + return None, None, None diff --git a/app/sites/siteuserinfo/torrent_leech.py b/app/sites/siteuserinfo/torrent_leech.py new file mode 100644 index 0000000..b2a1aef --- /dev/null +++ b/app/sites/siteuserinfo/torrent_leech.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class TorrentLeechSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.TorrentLeech + order = SITE_BASE_ORDER + 40 + + @classmethod + def match(cls, html_text): + return 'TorrentLeech' in html_text + + def _parse_site_page(self, html_text): + html_text = self._prepare_html_text(html_text) + + user_detail = re.search(r"/profile/([^/]+)/", html_text) + if user_detail and user_detail.group().strip(): + self._user_detail_page = user_detail.group().strip().lstrip('/') + self.userid = user_detail.group(1) + self._user_traffic_page = f"profile/{self.userid}/view" + self._torrent_seeding_page = f"profile/{self.userid}/seeding" + + def _parse_user_base_info(self, html_text): + self.username = self.userid + + def _parse_user_traffic_info(self, html_text): + """ + 上传/下载/分享率 [做种数/魔力值] + :param html_text: + :return: + """ + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + upload_html = html.xpath('//div[contains(@class,"profile-uploaded")]//span/text()') + if upload_html: + self.upload = StringUtils.num_filesize(upload_html[0]) + download_html = html.xpath('//div[contains(@class,"profile-downloaded")]//span/text()') + if download_html: + self.download = StringUtils.num_filesize(download_html[0]) + ratio_html = html.xpath('//div[contains(@class,"profile-ratio")]//span/text()') + if ratio_html: + self.ratio = StringUtils.str_float(ratio_html[0].replace('∞', '0')) + + user_level_html = html.xpath('//table[contains(@class, "profileViewTable")]' + '//tr/td[text()="Class"]/following-sibling::td/text()') + if user_level_html: + self.user_level = user_level_html[0].strip() + + join_at_html = html.xpath('//table[contains(@class, "profileViewTable")]' + '//tr/td[text()="Registration date"]/following-sibling::td/text()') + if join_at_html: + self.join_at = StringUtils.unify_datetime_str(join_at_html[0].strip()) + + bonus_html = html.xpath('//span[contains(@class, "total-TL-points")]/text()') + if bonus_html: + self.bonus = StringUtils.str_float(bonus_html[0].strip()) + + def _parse_user_detail_info(self, html_text): + pass + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(html_text) + if not html: + return None + + size_col = 2 + seeders_col = 7 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//tbody/tr/td[{size_col}]') + seeding_seeders = html.xpath(f'//tbody/tr/td[{seeders_col}]/text()') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i]) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + + return next_page + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/sites/siteuserinfo/unit3d.py b/app/sites/siteuserinfo/unit3d.py new file mode 100644 index 0000000..d33b454 --- /dev/null +++ b/app/sites/siteuserinfo/unit3d.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import re + +from lxml import etree + +from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER +from app.utils import StringUtils +from app.utils.types import SiteSchema + + +class Unit3dSiteUserInfo(_ISiteUserInfo): + schema = SiteSchema.Unit3d + order = SITE_BASE_ORDER + 15 + + @classmethod + def match(cls, html_text): + return "unit3d.js" in html_text + + def _parse_user_base_info(self, html_text): + html_text = self._prepare_html_text(html_text) + html = etree.HTML(html_text) + + tmps = html.xpath('//a[contains(@href, "/users/") and contains(@href, "settings")]/@href') + if tmps: + user_name_match = re.search(r"/users/(.+)/settings", tmps[0]) + if user_name_match and user_name_match.group().strip(): + self.username = user_name_match.group(1) + self._torrent_seeding_page = f"/users/{self.username}/active?perPage=100&client=&seeding=include" + self._user_detail_page = f"/users/{self.username}" + + tmps = html.xpath('//a[contains(@href, "bonus/earnings")]') + if tmps: + bonus_text = tmps[0].xpath("string(.)") + bonus_match = re.search(r"([\d,.]+)", bonus_text) + if bonus_match and bonus_match.group(1).strip(): + self.bonus = StringUtils.str_float(bonus_match.group(1)) + + def _parse_site_page(self, html_text): + # TODO + pass + + def _parse_user_detail_info(self, html_text): + """ + 解析用户额外信息,加入时间,等级 + :param html_text: + :return: + """ + html = etree.HTML(html_text) + if not html: + return None + + # 用户等级 + user_levels_text = html.xpath('//div[contains(@class, "content")]//span[contains(@class, "badge-user")]/text()') + if user_levels_text: + self.user_level = user_levels_text[0].strip() + + # 加入日期 + join_at_text = html.xpath('//div[contains(@class, "content")]//h4[contains(text(), "注册日期") ' + 'or contains(text(), "註冊日期") ' + 'or contains(text(), "Registration date")]/text()') + if join_at_text: + self.join_at = StringUtils.unify_datetime_str( + join_at_text[0].replace('注册日期', '').replace('註冊日期', '').replace('Registration date', '')) + + def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): + """ + 做种相关信息 + :param html_text: + :param multi_page: 是否多页数据 + :return: 下页地址 + """ + html = etree.HTML(html_text) + if not html: + return None + + size_col = 9 + seeders_col = 2 + # 搜索size列 + if html.xpath('//tr[position()=1]/th[contains(@class,"size")]'): + size_col = len(html.xpath('//tr[position()=1]/th[contains(@class,"size")]/preceding-sibling::th')) + 1 + # 搜索seeders列 + if html.xpath('//tr[position()=1]/th[contains(@class,"seeders")]'): + seeders_col = len(html.xpath('//tr[position()=1]/th[contains(@class,"seeders")]/preceding-sibling::th')) + 1 + + page_seeding = 0 + page_seeding_size = 0 + page_seeding_info = [] + seeding_sizes = html.xpath(f'//tr[position()]/td[{size_col}]') + seeding_seeders = html.xpath(f'//tr[position()]/td[{seeders_col}]') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = StringUtils.num_filesize(seeding_sizes[i].xpath("string(.)").strip()) + seeders = StringUtils.str_int(seeding_seeders[i].xpath("string(.)").strip()) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) + + self.seeding += page_seeding + self.seeding_size += page_seeding_size + self.seeding_info.extend(page_seeding_info) + + # 是否存在下页数据 + next_page = None + next_pages = html.xpath('//ul[@class="pagination"]/li[contains(@class,"active")]/following-sibling::li') + if next_pages and len(next_pages) > 1: + page_num = next_pages[0].xpath("string(.)").strip() + if page_num.isdigit(): + next_page = f"{self._torrent_seeding_page}&page={page_num}" + + return next_page + + def _parse_user_traffic_info(self, html_text): + html_text = self._prepare_html_text(html_text) + upload_match = re.search(r"[^总]上[传傳]量?[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+[KMGTPI]*B)", html_text, + re.IGNORECASE) + self.upload = StringUtils.num_filesize(upload_match.group(1).strip()) if upload_match else 0 + download_match = re.search(r"[^总子影力]下[载載]量?[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+[KMGTPI]*B)", html_text, + re.IGNORECASE) + self.download = StringUtils.num_filesize(download_match.group(1).strip()) if download_match else 0 + ratio_match = re.search(r"分享率[::_<>/a-zA-Z-=\"'\s#;]+([\d,.\s]+)", html_text) + self.ratio = StringUtils.str_float(ratio_match.group(1)) if ( + ratio_match and ratio_match.group(1).strip()) else 0.0 + + def _parse_message_unread_links(self, html_text, msg_links): + return None + + def _parse_message_content(self, html_text): + return None, None, None diff --git a/app/speedlimiter.py b/app/speedlimiter.py new file mode 100644 index 0000000..792e4f2 --- /dev/null +++ b/app/speedlimiter.py @@ -0,0 +1,212 @@ +from app.conf import SystemConfig +from app.downloader import Downloader +from app.mediaserver import MediaServer +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import DownloaderType, MediaServerType +from app.helper.security_helper import SecurityHelper +from apscheduler.schedulers.background import BackgroundScheduler +from config import Config + +import log + + +@singleton +class SpeedLimiter: + downloader = None + mediaserver = None + limit_enabled = False + limit_flag = False + qb_limit = False + qb_download_limit = 0 + qb_upload_limit = 0 + qb_upload_ratio = 0 + tr_limit = False + tr_download_limit = 0 + tr_upload_limit = 0 + tr_upload_ratio = 0 + unlimited_ips = {"ipv4": "0.0.0.0/0", "ipv6": "::/0"} + auto_limit = False + bandwidth = 0 + + _scheduler = None + + def __init__(self): + self.init_config() + + def init_config(self): + self.downloader = Downloader() + self.mediaserver = MediaServer() + + config = SystemConfig().get_system_config("SpeedLimit") + if config: + try: + self.bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000 + residual_ratio = float(config.get("residual_ratio") or 1) + if residual_ratio > 1: + residual_ratio = 1 + allocation = (config.get("allocation") or "1:1").split(":") + if len(allocation) != 2 or not str(allocation[0]).isdigit() or not str(allocation[-1]).isdigit(): + allocation = ["1", "1"] + self.qb_upload_ratio = round(int(allocation[0]) / (int(allocation[-1]) + int(allocation[0])) * residual_ratio, 2) + self.tr_upload_ratio = round(int(allocation[-1]) / (int(allocation[-1]) + int(allocation[0])) * residual_ratio, 2) + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.bandwidth = 0 + self.qb_upload_ratio = 0 + self.tr_upload_ratio = 0 + self.auto_limit = True if self.bandwidth and (self.qb_upload_ratio or self.tr_upload_ratio) else False + try: + self.qb_download_limit = int(float(config.get("qb_download") or 0)) * 1024 + self.qb_upload_limit = int(float(config.get("qb_upload") or 0)) * 1024 + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.qb_download_limit = 0 + self.qb_upload_limit = 0 + self.qb_limit = True if self.qb_download_limit or self.qb_upload_limit or self.auto_limit else False + try: + self.tr_download_limit = int(float(config.get("tr_download") or 0)) + self.tr_upload_limit = int(float(config.get("tr_upload") or 0)) + except Exception as e: + self.tr_download_limit = 0 + self.tr_upload_limit = 0 + ExceptionUtils.exception_traceback(e) + self.tr_limit = True if self.tr_download_limit or self.tr_upload_limit or self.auto_limit else False + self.limit_enabled = True if self.qb_limit or self.tr_limit else False + self.unlimited_ips["ipv4"] = config.get("ipv4") or "0.0.0.0/0" + self.unlimited_ips["ipv6"] = config.get("ipv6") or "::/0" + else: + self.limit_enabled = False + # 移出现有任务 + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + # 启动限速任务 + if self.limit_enabled: + self._scheduler = BackgroundScheduler(timezone=Config().get_timezone()) + self._scheduler.add_job(func=self.__check_playing_sessions, + args=[self.mediaserver.get_type(), True], + trigger='interval', + seconds=300) + self._scheduler.print_jobs() + self._scheduler.start() + log.info("播放限速服务启动") + + def __start(self): + """ + 开始限速 + """ + if self.qb_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.QB, + download_limit=self.qb_download_limit, + upload_limit=self.qb_upload_limit + ) + if not self.limit_flag: + log.info(f"【SpeedLimiter】Qbittorrent下载器开始限速") + if self.tr_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.TR, + download_limit=self.tr_download_limit, + upload_limit=self.tr_upload_limit + ) + if not self.limit_flag: + log.info(f"【SpeedLimiter】Transmission下载器开始限速") + self.limit_flag = True + + def __stop(self): + """ + 停止限速 + """ + if self.qb_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.QB, + download_limit=0, + upload_limit=0 + ) + if self.limit_flag: + log.info(f"【SpeedLimiter】Qbittorrent下载器停止限速") + if self.tr_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.TR, + download_limit=0, + upload_limit=0 + ) + if self.limit_flag: + log.info(f"【SpeedLimiter】Transmission下载器停止限速") + self.limit_flag = False + + def emby_action(self, message): + """ + 检查emby Webhook消息 + """ + if self.limit_enabled and message.get("Event") in ["playback.start", "playback.stop"]: + self.__check_playing_sessions(mediaserver_type=MediaServerType.EMBY, time_check=False) + + def jellyfin_action(self, message): + """ + 检查jellyfin Webhook消息 + """ + pass + + def plex_action(self, message): + """ + 检查plex Webhook消息 + """ + pass + + def __check_playing_sessions(self, mediaserver_type, time_check=False): + """ + 检查是否限速 + """ + if mediaserver_type != self.mediaserver.get_type(): + return + playing_sessions = self.mediaserver.get_playing_sessions() + limit_flag = False + if mediaserver_type == MediaServerType.EMBY: + total_bit_rate = 0 + for session in playing_sessions: + if not SecurityHelper.allow_access(self.unlimited_ips, session.get("RemoteEndPoint")) \ + and session.get("NowPlayingItem").get("MediaType") == "Video": + total_bit_rate += int(session.get("NowPlayingItem").get("Bitrate")) or 0 + if total_bit_rate: + limit_flag = True + if self.auto_limit: + residual_bandwidth = (self.bandwidth - total_bit_rate) + if residual_bandwidth < 0: + self.qb_upload_limit = 10*1024 + self.tr_upload_limit = 10 + else: + qb_upload_limit = residual_bandwidth / 8 / 1024 * self.qb_upload_ratio + tr_upload_limit = residual_bandwidth / 8 / 1024 * self.tr_upload_ratio + self.qb_upload_limit = qb_upload_limit * 1024 if qb_upload_limit > 10 else 10*1024 + self.tr_upload_limit = tr_upload_limit if tr_upload_limit > 10 else 10 + elif mediaserver_type == MediaServerType.JELLYFIN: + pass + elif mediaserver_type == MediaServerType.PLEX: + pass + else: + return + if time_check or self.auto_limit: + if limit_flag: + self.__start() + else: + self.__stop() + else: + if not self.limit_flag and limit_flag: + self.__start() + elif self.limit_flag and not limit_flag: + self.__stop() + else: + pass + + + + + + diff --git a/app/subscribe.py b/app/subscribe.py new file mode 100644 index 0000000..dc51c93 --- /dev/null +++ b/app/subscribe.py @@ -0,0 +1,852 @@ +import json +from threading import Lock + +import log +from app.downloader import Downloader +from app.filter import Filter +from app.helper import DbHelper, MetaHelper +from app.media import Media, DouBan +from app.media.meta import MetaInfo +from app.message import Message +from app.searcher import Searcher +from app.sites import Sites +from app.indexer import Indexer +from app.utils import Torrent +from app.utils.types import MediaType, SearchType +from web.backend.web_utils import WebUtils + +lock = Lock() + + +class Subscribe: + dbhelper = None + metahelper = None + searcher = None + message = None + media = None + downloader = None + sites = None + douban = None + filter = None + + def __init__(self): + self.dbhelper = DbHelper() + self.metahelper = MetaHelper() + self.searcher = Searcher() + self.message = Message() + self.media = Media() + self.downloader = Downloader() + self.sites = Sites() + self.douban = DouBan() + self.indexer = Indexer() + self.filter = Filter() + + def add_rss_subscribe(self, mtype, name, year, + keyword=None, + season=None, + fuzzy_match=False, + mediaid=None, + rss_sites=None, + search_sites=None, + over_edition=False, + filter_restype=None, + filter_pix=None, + filter_team=None, + filter_rule=None, + save_path=None, + download_setting=None, + total_ep=None, + current_ep=None, + state="D", + rssid=None): + """ + 添加电影、电视剧订阅 + :param mtype: 类型,电影、电视剧、动漫 + :param name: 标题 + :param year: 年份,如要是剧集需要是首播年份 + :param keyword: 自定义搜索词 + :param season: 第几季,数字 + :param fuzzy_match: 是否模糊匹配 + :param mediaid: 媒体ID,DB:/BG:/TMDBID + :param rss_sites: 订阅站点列表,为空则表示全部站点 + :param search_sites: 搜索站点列表,为空则表示全部站点 + :param over_edition: 是否选版 + :param filter_restype: 质量过滤 + :param filter_pix: 分辨率过滤 + :param filter_team: 制作组/字幕组过滤 + :param filter_rule: 关键字过滤 + :param save_path: 保存路径 + :param download_setting: 下载设置 + :param state: 添加订阅时的状态 + :param rssid: 修改订阅时传入 + :param total_ep: 总集数 + :param current_ep: 开始订阅集数 + :return: 错误码:0代表成功,错误信息 + """ + if not name: + return -1, "标题或类型有误", None + year = int(year) if str(year).isdigit() else "" + rss_sites = rss_sites or [] + search_sites = search_sites or [] + over_edition = 1 if over_edition else 0 + filter_rule = int(filter_rule) if str(filter_rule).isdigit() else None + total_ep = int(total_ep) if str(total_ep).isdigit() else None + current_ep = int(current_ep) if str(current_ep).isdigit() else None + download_setting = int(download_setting) if str(download_setting).replace("-", "").isdigit() else "" + fuzzy_match = True if fuzzy_match else False + # 检索媒体信息 + if not fuzzy_match: + # 根据TMDBID查询,从推荐加订阅的情况 + if mediaid: + # 根据ID查询 + media_info = WebUtils.get_mediainfo_from_id(mtype=mtype, mediaid=mediaid) + else: + # 根据名称和年份查询 + if season: + title = "%s %s 第%s季".strip() % (name, year, season) + else: + title = "%s %s".strip() % (name, year) + media_info = self.media.get_media_info(title=title, + mtype=mtype, + strict=True if year else False, + cache=False) + # 检查TMDB信息 + if not media_info or not media_info.tmdb_info: + return 1, "无法TMDB查询到媒体信息", None + # 添加订阅 + if media_info.type != MediaType.MOVIE: + # 电视剧 + if season: + total_episode = self.media.get_tmdb_season_episodes_num(tv_info=media_info.tmdb_info, + season=int(season)) + else: + # 查询季及集信息 + total_seasoninfo = self.media.get_tmdb_tv_seasons(tv_info=media_info.tmdb_info) + if not total_seasoninfo: + return 2, "获取剧集信息失败", media_info + # 按季号降序排序 + total_seasoninfo = sorted(total_seasoninfo, + key=lambda x: x.get("season_number"), + reverse=True) + # 取最新季 + season = total_seasoninfo[0].get("season_number") + total_episode = total_seasoninfo[0].get("episode_count") + if not total_episode: + return 3, "第%s季获取剧集数失败,请确认该季是否存在" % season, media_info + media_info.begin_season = int(season) + media_info.total_episodes = total_episode + if total_ep: + total = total_ep + else: + total = media_info.total_episodes + if current_ep: + lack = total - current_ep - 1 + else: + lack = total + if rssid: + self.dbhelper.delete_rss_tv(rssid=rssid) + code = self.dbhelper.insert_rss_tv(media_info=media_info, + total=total, + lack=lack, + state=state, + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + total_ep=total_ep, + current_ep=current_ep, + fuzzy_match=0, + desc=media_info.overview, + note=self.gen_rss_note(media_info), + keyword=keyword) + else: + # 电影 + if rssid: + self.dbhelper.delete_rss_movie(rssid=rssid) + code = self.dbhelper.insert_rss_movie(media_info=media_info, + state=state, + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + fuzzy_match=0, + desc=media_info.overview, + note=self.gen_rss_note(media_info), + keyword=keyword) + else: + # 模糊匹配 + media_info = MetaInfo(title=name, mtype=mtype) + media_info.title = name + media_info.type = mtype + if season: + media_info.begin_season = int(season) + if mtype == MediaType.MOVIE: + if rssid: + self.dbhelper.delete_rss_movie(rssid=rssid) + code = self.dbhelper.insert_rss_movie(media_info=media_info, + state="R", + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + fuzzy_match=1, + keyword=keyword) + else: + if rssid: + self.dbhelper.delete_rss_tv(rssid=rssid) + code = self.dbhelper.insert_rss_tv(media_info=media_info, + total=0, + lack=0, + state="R", + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + fuzzy_match=1, + keyword=keyword) + + if code == 0: + return code, "添加订阅成功", media_info + elif code == 9: + return code, "订阅已存在", media_info + else: + return code, "添加订阅失败", media_info + + def finish_rss_subscribe(self, rssid, media): + """ + 完成订阅 + :param rssid: 订阅ID + :param media: 识别的媒体信息,发送消息使用 + """ + if not rssid or not media: + return + # 电影订阅 + rtype = "MOV" if media.type == MediaType.MOVIE else "TV" + if media.type == MediaType.MOVIE: + # 查询电影RSS数据 + rss = self.dbhelper.get_rss_movies(rssid=rssid) + if not rss: + return + # 登记订阅历史 + self.dbhelper.insert_rss_history(rssid=rssid, + rtype=rtype, + name=rss[0].NAME, + year=rss[0].YEAR, + tmdbid=rss[0].TMDBID, + image=media.get_poster_image(), + desc=media.overview) + + # 删除订阅 + self.dbhelper.delete_rss_movie(rssid=rssid) + + # 电视剧订阅 + else: + # 查询电视剧RSS数据 + rss = self.dbhelper.get_rss_tvs(rssid=rssid) + if not rss: + return + total = rss[0].TOTAL_EP + # 登记订阅历史 + self.dbhelper.insert_rss_history(rssid=rssid, + rtype=rtype, + name=rss[0].NAME, + year=rss[0].YEAR, + season=rss[0].SEASON, + tmdbid=rss[0].TMDBID, + image=media.get_poster_image(), + desc=media.overview, + total=total if total else rss[0].TOTAL, + start=rss[0].CURRENT_EP) + # 删除订阅 + self.dbhelper.delete_rss_tv(rssid=rssid) + + # 发送订阅完成的消息 + log.info("【Rss】%s %s %s 订阅完成,删除订阅..." % ( + media.type.value, + media.get_title_string(), + media.get_season_string() + )) + self.message.send_rss_finished_message(media_info=media) + + def get_subscribe_movies(self, rid=None, state=None): + """ + 获取电影订阅 + """ + ret_dict = {} + rss_movies = self.dbhelper.get_rss_movies(rssid=rid, state=state) + rss_sites_valid = self.sites.get_site_names(rss=True) + search_sites_valid = self.indexer.get_indexer_names() + for rss_movie in rss_movies: + desc = rss_movie.DESC + note = rss_movie.NOTE + tmdbid = rss_movie.TMDBID + rss_sites = json.loads(rss_movie.RSS_SITES) if rss_movie.RSS_SITES else [] + search_sites = json.loads(rss_movie.SEARCH_SITES) if rss_movie.SEARCH_SITES else [] + over_edition = True if rss_movie.OVER_EDITION == 1 else False + filter_restype = rss_movie.FILTER_RESTYPE + filter_pix = rss_movie.FILTER_PIX + filter_team = rss_movie.FILTER_TEAM + filter_rule = rss_movie.FILTER_RULE + download_setting = rss_movie.DOWNLOAD_SETTING + save_path = rss_movie.SAVE_PATH + fuzzy_match = True if rss_movie.FUZZY_MATCH == 1 else False + keyword = rss_movie.KEYWORD + # 兼容旧配置 + if desc and desc.find('{') != -1: + desc = self.__parse_rss_desc(desc) + rss_sites = desc.get("rss_sites") + search_sites = desc.get("search_sites") + over_edition = True if desc.get("over_edition") == 'Y' else False + filter_restype = desc.get("restype") + filter_pix = desc.get("pix") + filter_team = desc.get("team") + filter_rule = desc.get("rule") + download_setting = "" + save_path = "" + fuzzy_match = False if tmdbid else True + if note: + note_info = self.__parse_rss_desc(note) + else: + note_info = {} + rss_sites = [site for site in rss_sites if site in rss_sites_valid] + search_sites = [site for site in search_sites if site in search_sites_valid] + ret_dict[str(rss_movie.ID)] = { + "id": rss_movie.ID, + "name": rss_movie.NAME, + "year": rss_movie.YEAR, + "tmdbid": rss_movie.TMDBID, + "image": rss_movie.IMAGE, + "overview": rss_movie.DESC, + "rss_sites": rss_sites, + "search_sites": search_sites, + "over_edition": over_edition, + "filter_restype": filter_restype, + "filter_pix": filter_pix, + "filter_team": filter_team, + "filter_rule": filter_rule, + "save_path": save_path, + "download_setting": download_setting, + "fuzzy_match": fuzzy_match, + "state": rss_movie.STATE, + "poster": note_info.get("poster"), + "release_date": note_info.get("release_date"), + "vote": note_info.get("vote"), + "keyword": keyword + + } + return ret_dict + + def get_subscribe_tvs(self, rid=None, state=None): + ret_dict = {} + rss_tvs = self.dbhelper.get_rss_tvs(rssid=rid, state=state) + rss_sites_valid = self.sites.get_site_names(rss=True) + search_sites_valid = self.indexer.get_indexer_names() + for rss_tv in rss_tvs: + desc = rss_tv.DESC + note = rss_tv.NOTE + tmdbid = rss_tv.TMDBID + rss_sites = json.loads(rss_tv.RSS_SITES) if rss_tv.RSS_SITES else [] + search_sites = json.loads(rss_tv.SEARCH_SITES) if rss_tv.SEARCH_SITES else [] + over_edition = True if rss_tv.OVER_EDITION == 1 else False + filter_restype = rss_tv.FILTER_RESTYPE + filter_pix = rss_tv.FILTER_PIX + filter_team = rss_tv.FILTER_TEAM + filter_rule = rss_tv.FILTER_RULE + download_setting = rss_tv.DOWNLOAD_SETTING + save_path = rss_tv.SAVE_PATH + total_ep = rss_tv.TOTAL_EP + current_ep = rss_tv.CURRENT_EP + fuzzy_match = True if rss_tv.FUZZY_MATCH == 1 else False + keyword = rss_tv.KEYWORD + # 兼容旧配置 + if desc and desc.find('{') != -1: + desc = self.__parse_rss_desc(desc) + rss_sites = desc.get("rss_sites") + search_sites = desc.get("search_sites") + over_edition = True if desc.get("over_edition") == 'Y' else False + filter_restype = desc.get("restype") + filter_pix = desc.get("pix") + filter_team = desc.get("team") + filter_rule = desc.get("rule") + save_path = "" + download_setting = "" + total_ep = desc.get("total") + current_ep = desc.get("current") + fuzzy_match = False if tmdbid else True + if note: + note_info = self.__parse_rss_desc(note) + else: + note_info = {} + rss_sites = [site for site in rss_sites if site in rss_sites_valid] + search_sites = [site for site in search_sites if site in search_sites_valid] + ret_dict[str(rss_tv.ID)] = { + "id": rss_tv.ID, + "name": rss_tv.NAME, + "year": rss_tv.YEAR, + "season": rss_tv.SEASON, + "tmdbid": rss_tv.TMDBID, + "image": rss_tv.IMAGE, + "overview": rss_tv.DESC, + "rss_sites": rss_sites, + "search_sites": search_sites, + "over_edition": over_edition, + "filter_restype": filter_restype, + "filter_pix": filter_pix, + "filter_team": filter_team, + "filter_rule": filter_rule, + "save_path": save_path, + "download_setting": download_setting, + "total": rss_tv.TOTAL, + "lack": rss_tv.LACK, + "total_ep": total_ep, + "current_ep": current_ep, + "fuzzy_match": fuzzy_match, + "state": rss_tv.STATE, + "poster": note_info.get("poster"), + "release_date": note_info.get("release_date"), + "vote": note_info.get("vote"), + "keyword": keyword + } + return ret_dict + + @staticmethod + def __parse_rss_desc(desc): + """ + 解析订阅的JSON字段 + """ + if not desc: + return {} + return json.loads(desc) or {} + + @staticmethod + def gen_rss_note(media): + """ + 生成订阅的JSON备注信息 + :param media: 媒体信息 + :return: 备注信息 + """ + if not media: + return {} + note = { + "poster": media.get_poster_image(), + "release_date": media.release_date, + "vote": media.vote_average + } + return json.dumps(note) + + def refresh_rss_metainfo(self): + """ + 定时将豆瓣订阅转换为TMDB的订阅,并更新订阅的TMDB信息 + """ + # 更新电影 + log.info("【Subscribe】开始刷新订阅TMDB信息...") + rss_movies = self.get_subscribe_movies(state='R') + for rid, rss_info in rss_movies.items(): + # 跳过模糊匹配的 + if rss_info.get("fuzzy_match"): + continue + rssid = rss_info.get("id") + name = rss_info.get("name") + year = rss_info.get("year") or "" + tmdbid = rss_info.get("tmdbid") + # 更新TMDB信息 + media_info = self.__get_media_info(tmdbid=tmdbid, + name=name, + year=year, + mtype=MediaType.MOVIE, + cache=False) + if media_info and media_info.tmdb_id and media_info.title != name: + log.info(f"【Subscribe】检测到TMDB信息变化,更新电影订阅 {name} 为 {media_info.title}") + # 更新订阅信息 + self.dbhelper.update_rss_movie_tmdb(rid=rssid, + tmdbid=media_info.tmdb_id, + title=media_info.title, + year=media_info.year, + image=media_info.get_message_image(), + desc=media_info.overview, + note=self.gen_rss_note(media_info)) + # 清除TMDB缓存 + self.metahelper.delete_meta_data_by_tmdbid(media_info.tmdb_id) + + # 更新电视剧 + rss_tvs = self.get_subscribe_tvs(state='R') + for rid, rss_info in rss_tvs.items(): + # 跳过模糊匹配的 + if rss_info.get("fuzzy_match"): + continue + rssid = rss_info.get("id") + name = rss_info.get("name") + year = rss_info.get("year") or "" + tmdbid = rss_info.get("tmdbid") + season = rss_info.get("season") or 1 + total = rss_info.get("total") + total_ep = rss_info.get("total_ep") + lack = rss_info.get("lack") + # 更新TMDB信息 + media_info = self.__get_media_info(tmdbid=tmdbid, + name=name, + year=year, + mtype=MediaType.TV, + cache=False) + if media_info and media_info.tmdb_id: + # 获取总集数 + total_episode = self.media.get_tmdb_season_episodes_num(tv_info=media_info.tmdb_info, + season=int(str(season).replace("S", ""))) + # 设置总集数的,不更新集数 + if total_ep: + total_episode = total_ep + if total_episode and (name != media_info.title or total != total_episode): + # 新的缺失集数 + lack_episode = total_episode - (total - lack) + log.info( + f"【Subscribe】检测到TMDB信息变化,更新电视剧订阅 {name} 为 {media_info.title},总集数为:{total_episode}") + # 更新订阅信息 + self.dbhelper.update_rss_tv_tmdb(rid=rssid, + tmdbid=media_info.tmdb_id, + title=media_info.title, + year=media_info.year, + total=total_episode, + lack=lack_episode, + image=media_info.get_message_image(), + desc=media_info.overview, + note=self.gen_rss_note(media_info)) + # 更新缺失季集 + self.dbhelper.update_rss_tv_episodes(rid=rssid, episodes=range(total - lack + 1, total + 1)) + # 清除TMDB缓存 + self.metahelper.delete_meta_data_by_tmdbid(media_info.tmdb_id) + log.info("【Subscribe】订阅TMDB信息刷新完成") + + def __get_media_info(self, tmdbid, name, year, mtype, cache=True): + """ + 综合返回媒体信息 + """ + if tmdbid and not str(tmdbid).startswith("DB:"): + media_info = MetaInfo(title="%s %s".strip() % (name, year)) + tmdb_info = self.media.get_tmdb_info(mtype=mtype, tmdbid=tmdbid) + media_info.set_tmdb_info(tmdb_info) + else: + media_info = self.media.get_media_info(title="%s %s" % (name, year), mtype=mtype, strict=True, cache=cache) + return media_info + + def subscribe_search_all(self): + """ + 搜索R状态的所有订阅,由定时服务调用 + """ + self.subscribe_search(state="R") + + def subscribe_search(self, state="D"): + """ + RSS订阅队列中状态的任务处理,先进行存量资源检索,缺失的才标志为RSS状态,由定时服务调用 + """ + try: + lock.acquire() + # 处理电影 + self.subscribe_search_movie(state=state) + # 处理电视剧 + self.subscribe_search_tv(state=state) + finally: + lock.release() + + def subscribe_search_movie(self, rssid=None, state='D'): + """ + 检索电影RSS + :param rssid: 订阅ID,未输入时检索所有状态为D的,输入时检索该ID任何状态的 + :param state: 检索的状态,默认为队列中才检索 + """ + if rssid: + rss_movies = self.get_subscribe_movies(rid=rssid) + else: + rss_movies = self.get_subscribe_movies(state=state) + if rss_movies: + log.info("【Subscribe】共有 %s 个电影订阅需要检索" % len(rss_movies)) + for rid, rss_info in rss_movies.items(): + # 跳过模糊匹配的 + if rss_info.get("fuzzy_match"): + continue + # 搜索站点范围 + rssid = rss_info.get("id") + name = rss_info.get("name") + year = rss_info.get("year") or "" + tmdbid = rss_info.get("tmdbid") + over_edition = rss_info.get("over_edition") + keyword = rss_info.get("keyword") + + # 开始搜索 + self.dbhelper.update_rss_movie_state(rssid=rssid, state='S') + # 识别 + media_info = self.__get_media_info(tmdbid, name, year, MediaType.MOVIE) + # 未识别到媒体信息 + if not media_info or not media_info.tmdb_info: + self.dbhelper.update_rss_movie_state(rssid=rssid, state='R') + continue + media_info.set_download_info(download_setting=rss_info.get("download_setting"), + save_path=rss_info.get("save_path")) + # 自定义搜索词 + media_info.keyword = keyword + # 非洗版的情况检查是否存在 + if not over_edition: + # 检查是否存在 + exist_flag, no_exists, _ = self.downloader.check_exists_medias(meta_info=media_info) + # 已经存在 + if exist_flag: + log.info("【Subscribe】电影 %s 已存在" % media_info.get_title_string()) + self.finish_rss_subscribe(rssid=rssid, media=media_info) + continue + else: + # 洗版时按缺失来下载 + no_exists = {} + # 把洗版标志加入检索 + media_info.over_edition = over_edition + # 将当前的优先级传入搜索 + media_info.res_order = self.dbhelper.get_rss_overedition_order(rtype=media_info.type, + rssid=rssid) + # 开始检索 + filter_dict = { + "restype": rss_info.get('filter_restype'), + "pix": rss_info.get('filter_pix'), + "team": rss_info.get('filter_team'), + "rule": rss_info.get('filter_rule'), + "site": rss_info.get("search_sites") + } + search_result, _, _, _ = self.searcher.search_one_media( + media_info=media_info, + in_from=SearchType.RSS, + no_exists=no_exists, + sites=rss_info.get("search_sites"), + filters=filter_dict) + if search_result: + # 洗版 + if over_edition: + self.update_subscribe_over_edition(rtype=search_result.type, + rssid=rssid, + media=search_result) + else: + self.finish_rss_subscribe(rssid=rssid, media=media_info) + else: + self.dbhelper.update_rss_movie_state(rssid=rssid, state='R') + + def subscribe_search_tv(self, rssid=None, state="D"): + """ + 检索电视剧RSS + :param rssid: 订阅ID,未输入时检索所有状态为D的,输入时检索该ID任何状态的 + :param state: 检索的状态,默认为队列中才检索 + """ + if rssid: + rss_tvs = self.get_subscribe_tvs(rid=rssid) + else: + rss_tvs = self.get_subscribe_tvs(state=state) + if rss_tvs: + log.info("【Subscribe】共有 %s 个电视剧订阅需要检索" % len(rss_tvs)) + rss_no_exists = {} + for rid, rss_info in rss_tvs.items(): + # 跳过模糊匹配的 + if rss_info.get("fuzzy_match"): + continue + rssid = rss_info.get("id") + name = rss_info.get("name") + year = rss_info.get("year") or "" + tmdbid = rss_info.get("tmdbid") + over_edition = rss_info.get("over_edition") + keyword = rss_info.get("keyword") + # 开始搜索 + self.dbhelper.update_rss_tv_state(rssid=rssid, state='S') + # 识别 + media_info = self.__get_media_info(tmdbid, name, year, MediaType.TV) + # 未识别到媒体信息 + if not media_info or not media_info.tmdb_info: + self.dbhelper.update_rss_tv_state(rssid=rssid, state='R') + continue + # 取下载设置 + media_info.set_download_info(download_setting=rss_info.get("download_setting"), + save_path=rss_info.get("save_path")) + # 从登记薄中获取缺失剧集 + season = 1 + if rss_info.get("season"): + season = int(str(rss_info.get("season")).replace("S", "")) + # 订阅季 + media_info.begin_season = season + # 订阅ID + media_info.rssid = rssid + # 自定义集数 + total_ep = rss_info.get("total") + current_ep = rss_info.get("current_ep") + # 自定义搜索词 + media_info.keyword = keyword + # 表中记录的剩余订阅集数 + episodes = self.get_subscribe_tv_episodes(rss_info.get("id")) + if episodes is None: + episodes = [] + if current_ep: + episodes = list(range(current_ep, total_ep + 1)) + rss_no_exists[media_info.tmdb_id] = [ + { + "season": season, + "episodes": episodes, + "total_episodes": total_ep + } + ] + else: + rss_no_exists[media_info.tmdb_id] = [ + { + "season": season, + "episodes": episodes, + "total_episodes": total_ep + } + ] + # 非洗版时检查本地媒体库情况 + if not over_edition: + exist_flag, library_no_exists, _ = self.downloader.check_exists_medias( + meta_info=media_info, + total_ep={season: total_ep}) + # 当前剧集已存在,跳过 + if exist_flag: + # 已全部存在 + if not library_no_exists \ + or not library_no_exists.get(media_info.tmdb_id): + log.info("【Subscribe】电视剧 %s 订阅剧集已全部存在" % ( + media_info.get_title_string())) + # 完成订阅 + self.finish_rss_subscribe(rssid=rss_info.get("id"), + media=media_info) + continue + # 取交集做为缺失集 + rss_no_exists = Torrent.get_intersection_episodes(target=rss_no_exists, + source=library_no_exists, + title=media_info.tmdb_id) + if rss_no_exists.get(media_info.tmdb_id): + log.info("【Subscribe】%s 订阅缺失季集:%s" % ( + media_info.get_title_string(), + rss_no_exists.get(media_info.tmdb_id) + )) + else: + # 把洗版标志加入检索 + media_info.over_edition = over_edition + # 将当前的优先级传入检索 + media_info.res_order = self.dbhelper.get_rss_overedition_order(rtype=MediaType.TV, + rssid=rssid) + + # 开始检索 + filter_dict = { + "restype": rss_info.get('filter_restype'), + "pix": rss_info.get('filter_pix'), + "team": rss_info.get('filter_team'), + "rule": rss_info.get('filter_rule'), + "site": rss_info.get("search_sites") + } + search_result, no_exists, _, _ = self.searcher.search_one_media( + media_info=media_info, + in_from=SearchType.RSS, + no_exists=rss_no_exists, + sites=rss_info.get("search_sites"), + filters=filter_dict) + if search_result \ + or not no_exists \ + or not no_exists.get(media_info.tmdb_id): + # 洗版 + if over_edition: + self.update_subscribe_over_edition(rtype=media_info.type, + rssid=rssid, + media=search_result) + else: + # 完成订阅 + self.finish_rss_subscribe(rssid=rssid, media=media_info) + elif no_exists: + # 更新状态 + self.update_subscribe_tv_lack(rssid=rssid, + media_info=media_info, + seasoninfo=no_exists.get(media_info.tmdb_id)) + + def update_rss_state(self, rtype, rssid, state): + """ + 根据类型更新订阅状态 + :param rtype: 订阅类型 + :param rssid: 订阅ID + :param state: 状态 R/D/S + """ + if rtype == MediaType.MOVIE: + self.dbhelper.update_rss_movie_state(rssid=rssid, state=state) + else: + self.dbhelper.update_rss_tv_state(rssid=rssid, state=state) + + def update_subscribe_over_edition(self, rtype, rssid, media): + """ + 更新洗版订阅 + :param rtype: 订阅类型 + :param rssid: 订阅ID + :param media: 含订阅信息的媒体信息 + :return 完成订阅返回True,否则返回False + """ + if not rssid \ + or not media.res_order \ + or not media.filter_rule \ + or not media.res_order: + return False + # 更新订阅命中的优先级 + self.dbhelper.update_rss_filter_order(rtype=media.type, + rssid=rssid, + res_order=media.res_order) + # 检查是否匹配最高优先级规则 + over_edition_order = self.filter.get_rule_first_order(rulegroup=media.filter_rule) + if int(media.res_order) >= int(over_edition_order): + # 完成洗版订阅 + self.finish_rss_subscribe(rssid=rssid, media=media) + return True + else: + self.update_rss_state(rtype=rtype, rssid=rssid, state='R') + return False + + def check_subscribe_over_edition(self, rtype, rssid, res_order): + """ + 检查洗版订阅的优先级 + :param rtype: 订阅类型 + :param rssid: 订阅ID + :param res_order: 优先级 + :return 资源更优先返回True,否则返回False + """ + pre_res_order = self.dbhelper.get_rss_overedition_order(rtype=rtype, rssid=rssid) + if not pre_res_order: + return True + return True if int(pre_res_order) < int(res_order) else False + + def update_subscribe_tv_lack(self, rssid, media_info, seasoninfo): + """ + 更新电视剧订阅缺失集数 + """ + if not seasoninfo: + return + self.dbhelper.update_rss_tv_state(rssid=rssid, state='R') + for info in seasoninfo: + if str(info.get("season")) == media_info.get_season_seq(): + if info.get("episodes"): + log.info("【Subscribe】更新电视剧 %s %s 缺失集数为 %s" % ( + media_info.get_title_string(), + media_info.get_season_string(), + len(info.get("episodes")))) + self.dbhelper.update_rss_tv_lack(rssid=rssid, lack_episodes=info.get("episodes")) + break + + def get_subscribe_tv_episodes(self, rssid): + """ + 查询数据库中订阅的电视剧缺失集数 + """ + return self.dbhelper.get_rss_tv_episodes(rssid) diff --git a/app/subtitle.py b/app/subtitle.py new file mode 100644 index 0000000..d256116 --- /dev/null +++ b/app/subtitle.py @@ -0,0 +1,363 @@ +import datetime +import os.path +import re +import shutil + +from lxml import etree + +import log +from app.conf import SiteConf +from app.helper import OpenSubtitles +from app.utils import RequestUtils, PathUtils, SystemUtils, StringUtils, ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import MediaType +from config import Config, RMT_SUBEXT + + +@singleton +class Subtitle: + opensubtitles = None + _save_tmp_path = None + _server = None + _host = None + _api_key = None + _remote_path = None + _local_path = None + _opensubtitles_enable = False + + def __init__(self): + self.init_config() + + def init_config(self): + self.opensubtitles = OpenSubtitles() + self._save_tmp_path = Config().get_temp_path() + if not os.path.exists(self._save_tmp_path): + os.makedirs(self._save_tmp_path) + subtitle = Config().get_config('subtitle') + if subtitle: + self._server = subtitle.get("server") + if self._server == "chinesesubfinder": + self._api_key = subtitle.get("chinesesubfinder", {}).get("api_key") + self._host = subtitle.get("chinesesubfinder", {}).get('host') + if self._host: + if not self._host.startswith('http'): + self._host = "http://" + self._host + if not self._host.endswith('/'): + self._host = self._host + "/" + self._local_path = subtitle.get("chinesesubfinder", {}).get("local_path") + self._remote_path = subtitle.get("chinesesubfinder", {}).get("remote_path") + else: + self._opensubtitles_enable = subtitle.get("opensubtitles", {}).get("enable") + + def download_subtitle(self, items, server=None): + """ + 字幕下载入口 + :param items: {"type":, "file", "file_ext":, "name":, "title", "year":, "season":, "episode":, "bluray":} + :param server: 字幕下载服务器 + :return: 是否成功,消息内容 + """ + if not items: + return False, "参数有误" + _server = self._server if not server else server + if not _server: + return False, "未配置字幕下载器" + if _server == "opensubtitles": + if server or self._opensubtitles_enable: + return self.__download_opensubtitles(items) + elif _server == "chinesesubfinder": + return self.__download_chinesesubfinder(items) + return False, "未配置字幕下载器" + + def __search_opensubtitles(self, item): + """ + 爬取OpenSubtitles.org字幕 + """ + if not self.opensubtitles: + return [] + return self.opensubtitles.search_subtitles(item) + + def __download_opensubtitles(self, items): + """ + 调用OpenSubtitles Api下载字幕 + """ + if not self.opensubtitles: + return False, "未配置OpenSubtitles" + subtitles_cache = {} + success = False + ret_msg = "" + for item in items: + if not item: + continue + if not item.get("name") or not item.get("file"): + continue + if item.get("type") == MediaType.TV and not item.get("imdbid"): + log.warn("【Subtitle】电视剧类型需要imdbid检索字幕,跳过...") + ret_msg = "电视剧需要imdbid检索字幕" + continue + subtitles = subtitles_cache.get(item.get("name")) + if subtitles is None: + log.info( + "【Subtitle】开始从Opensubtitle.org检索字幕: %s,imdbid=%s" % (item.get("name"), item.get("imdbid"))) + subtitles = self.__search_opensubtitles(item) + if not subtitles: + subtitles_cache[item.get("name")] = [] + log.info("【Subtitle】%s 未检索到字幕" % item.get("name")) + ret_msg = "%s 未检索到字幕" % item.get("name") + else: + subtitles_cache[item.get("name")] = subtitles + log.info("【Subtitle】opensubtitles.org返回数据:%s" % len(subtitles)) + if not subtitles: + continue + # 成功数 + subtitle_count = 0 + for subtitle in subtitles: + # 标题 + if not item.get("imdbid"): + if str(subtitle.get('title')) != "%s (%s)" % (item.get("name"), item.get("year")): + continue + # 季 + if item.get('season') \ + and str(subtitle.get('season').replace("Season", "").strip()) != str(item.get('season')): + continue + # 集 + if item.get('episode') \ + and str(subtitle.get('episode')) != str(item.get('episode')): + continue + # 字幕文件名 + SubFileName = subtitle.get('description') + # 下载链接 + Download_Link = subtitle.get('link') + # 下载后的字幕文件路径 + Media_File = "%s.chi.zh-cn%s" % (item.get("file"), item.get("file_ext")) + log.info("【Subtitle】正在从opensubtitles.org下载字幕 %s 到 %s " % (SubFileName, Media_File)) + # 下载 + ret = RequestUtils(cookies=self.opensubtitles.get_cookie(), + headers=self.opensubtitles.get_ua()).get_res(Download_Link) + if ret and ret.status_code == 200: + # 保存ZIP + file_name = self.__get_url_subtitle_name(ret.headers.get('content-disposition'), Download_Link) + if not file_name: + continue + zip_file = os.path.join(self._save_tmp_path, file_name) + zip_path = os.path.splitext(zip_file)[0] + with open(zip_file, 'wb') as f: + f.write(ret.content) + # 解压文件 + shutil.unpack_archive(zip_file, zip_path, format='zip') + # 遍历转移文件 + for sub_file in PathUtils.get_dir_files(in_path=zip_path, exts=RMT_SUBEXT): + self.__transfer_subtitle(sub_file, Media_File) + # 删除临时文件 + try: + shutil.rmtree(zip_path) + os.remove(zip_file) + except Exception as err: + ExceptionUtils.exception_traceback(err) + else: + log.error("【Subtitle】下载字幕文件失败:%s" % Download_Link) + continue + # 最多下载3个字幕 + subtitle_count += 1 + if subtitle_count > 2: + break + if not subtitle_count: + if item.get('episode'): + log.info("【Subtitle】%s 第%s季 第%s集 未找到符合条件的字幕" % ( + item.get("name"), item.get("season"), item.get("episode"))) + ret_msg = "%s 第%s季 第%s集 未找到符合条件的字幕" % ( + item.get("name"), item.get("season"), item.get("episode")) + else: + log.info("【Subtitle】%s 未找到符合条件的字幕" % item.get("name")) + ret_msg = "%s 未找到符合条件的字幕" % item.get("name") + else: + log.info("【Subtitle】%s 共下载了 %s 个字幕" % (item.get("name"), subtitle_count)) + ret_msg = "%s 共下载了 %s 个字幕" % (item.get("name"), subtitle_count) + success = True + if success: + return True, ret_msg + else: + return False, ret_msg + + def __download_chinesesubfinder(self, items): + """ + 调用ChineseSubFinder下载字幕 + """ + if not self._host or not self._api_key: + return False, "未配置ChineseSubFinder" + req_url = "%sapi/v1/add-job" % self._host + notify_items = [] + success = False + ret_msg = "" + for item in items: + if not item: + continue + if not item.get("name") or not item.get("file"): + continue + if item.get("bluray"): + file_path = "%s.mp4" % item.get("file") + else: + if os.path.splitext(item.get("file"))[-1] != item.get("file_ext"): + file_path = "%s%s" % (item.get("file"), item.get("file_ext")) + else: + file_path = item.get("file") + + # 路径替换 + if self._local_path and self._remote_path and file_path.startswith(self._local_path): + file_path = file_path.replace(self._local_path, self._remote_path).replace('\\', '/') + + # 一个名称只建一个任务 + if file_path not in notify_items: + notify_items.append(file_path) + log.info("【Subtitle】通知ChineseSubFinder下载字幕: %s" % file_path) + params = { + "video_type": 0 if item.get("type") == MediaType.MOVIE else 1, + "physical_video_file_full_path": file_path, + "task_priority_level": 3, + "media_server_inside_video_id": "", + "is_bluray": item.get("bluray") + } + try: + res = RequestUtils(headers={ + "Authorization": "Bearer %s" % self._api_key + }).post(req_url, json=params) + if not res or res.status_code != 200: + log.error("【Subtitle】调用ChineseSubFinder API失败!") + ret_msg = "调用ChineseSubFinder API失败" + else: + # 如果文件目录没有识别的nfo元数据, 此接口会返回控制符,推测是ChineseSubFinder的原因 + # emby refresh元数据时异步的 + if res.text: + job_id = res.json().get("job_id") + message = res.json().get("message") + if not job_id: + log.warn("【Subtitle】ChineseSubFinder下载字幕出错:%s" % message) + ret_msg = "ChineseSubFinder下载字幕出错:%s" % message + else: + log.info("【Subtitle】ChineseSubFinder任务添加成功:%s" % job_id) + ret_msg = "ChineseSubFinder任务添加成功:%s" % job_id + success = True + else: + log.error("【Subtitle】%s 目录缺失nfo元数据" % file_path) + ret_msg = "%s 目录下缺失nfo元数据:" % file_path + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【Subtitle】连接ChineseSubFinder出错:" + str(e)) + ret_msg = "连接ChineseSubFinder出错:%s" % str(e) + if success: + return True, ret_msg + else: + return False, ret_msg + + @staticmethod + def __transfer_subtitle(sub_file, media_file): + """ + 转移字幕 + """ + new_sub_file = "%s%s" % (os.path.splitext(media_file)[0], os.path.splitext(sub_file)[-1]) + if os.path.exists(new_sub_file): + return 1 + else: + return SystemUtils.copy(sub_file, new_sub_file) + + def download_subtitle_from_site(self, media_info, cookie, ua, download_dir): + """ + 从站点下载字幕文件,并保存到本地 + """ + if not media_info.page_url: + return + # 字幕下载目录 + log.info("【Subtitle】开始从站点下载字幕:%s" % media_info.page_url) + if not download_dir: + log.warn("【Subtitle】未找到字幕下载目录") + return + # 读取网站代码 + request = RequestUtils(cookies=cookie, headers=ua) + res = request.get_res(media_info.page_url) + if res and res.status_code == 200: + if not res.text: + log.warn(f"【Subtitle】读取页面代码失败:{media_info.page_url}") + return + html = etree.HTML(res.text) + sublink_list = [] + for xpath in SiteConf.SITE_SUBTITLE_XPATH: + sublinks = html.xpath(xpath) + if sublinks: + for sublink in sublinks: + if not sublink: + continue + if not sublink.startswith("http"): + base_url = StringUtils.get_base_url(media_info.page_url) + if sublink.startswith("/"): + sublink = "%s%s" % (base_url, sublink) + else: + sublink = "%s/%s" % (base_url, sublink) + sublink_list.append(sublink) + # 下载所有字幕文件 + for sublink in sublink_list: + log.info(f"【Subtitle】找到字幕下载链接:{sublink},开始下载...") + # 下载 + ret = request.get_res(sublink) + if ret and ret.status_code == 200: + # 创建目录 + if not os.path.exists(download_dir): + os.makedirs(download_dir) + # 保存ZIP + file_name = self.__get_url_subtitle_name(ret.headers.get('content-disposition'), sublink) + if not file_name: + log.warn(f"【Subtitle】链接不是字幕文件:{sublink}") + continue + if file_name.lower().endswith(".zip"): + # ZIP包 + zip_file = os.path.join(self._save_tmp_path, file_name) + # 解压路径 + zip_path = os.path.splitext(zip_file)[0] + with open(zip_file, 'wb') as f: + f.write(ret.content) + # 解压文件 + shutil.unpack_archive(zip_file, zip_path, format='zip') + # 遍历转移文件 + for sub_file in PathUtils.get_dir_files(in_path=zip_path, exts=RMT_SUBEXT): + target_sub_file = os.path.join(download_dir, + os.path.splitext(os.path.basename(sub_file))[0]) + log.info(f"【Subtitle】转移字幕 {sub_file} 到 {target_sub_file}") + self.__transfer_subtitle(sub_file, target_sub_file) + # 删除临时文件 + try: + shutil.rmtree(zip_path) + os.remove(zip_file) + except Exception as err: + ExceptionUtils.exception_traceback(err) + else: + sub_file = os.path.join(self._save_tmp_path, file_name) + # 保存 + with open(sub_file, 'wb') as f: + f.write(ret.content) + target_sub_file = os.path.join(download_dir, + os.path.splitext(os.path.basename(sub_file))[0]) + log.info(f"【Subtitle】转移字幕 {sub_file} 到 {target_sub_file}") + self.__transfer_subtitle(sub_file, target_sub_file) + else: + log.error(f"【Subtitle】下载字幕文件失败:{sublink}") + continue + if sublink_list: + log.info(f"【Subtitle】{media_info.page_url} 页面字幕下载完成") + elif res is not None: + log.warn(f"【Subtitle】连接 {media_info.page_url} 失败,状态码:{res.status_code}") + else: + log.warn(f"【Subtitle】无法打开链接:{media_info.page_url}") + + @staticmethod + def __get_url_subtitle_name(disposition, url): + """ + 从下载请求中获取字幕文件名 + """ + file_name = re.findall(r"filename=\"?(.+)\"?", disposition or "") + if file_name: + file_name = str(file_name[0].encode('ISO-8859-1').decode()).split(";")[0].strip() + if file_name.endswith('"'): + file_name = file_name[:-1] + elif url and os.path.splitext(url)[-1] in (RMT_SUBEXT + ['.zip']): + file_name = url.split("/")[-1] + else: + file_name = str(datetime.datetime.now()) + return file_name diff --git a/app/sync.py b/app/sync.py new file mode 100644 index 0000000..e193705 --- /dev/null +++ b/app/sync.py @@ -0,0 +1,394 @@ +import os +import threading +import traceback + +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer +from watchdog.observers.polling import PollingObserver + +import log +from app.conf import ModuleConf +from app.helper import DbHelper +from config import RMT_MEDIAEXT, Config +from app.filetransfer import FileTransfer +from app.utils.commons import singleton +from app.utils import PathUtils, ExceptionUtils +from app.utils.types import SyncType, OsType + +lock = threading.Lock() + + +class FileMonitorHandler(FileSystemEventHandler): + """ + 目录监控响应类 + """ + + def __init__(self, monpath, sync, **kwargs): + super(FileMonitorHandler, self).__init__(**kwargs) + self._watch_path = monpath + self.sync = sync + + def on_created(self, event): + self.sync.file_change_handler(event, "创建", event.src_path) + + def on_moved(self, event): + self.sync.file_change_handler(event, "移动", event.dest_path) + + """ + def on_modified(self, event): + self.sync.file_change_handler(event, "修改", event.src_path) + """ + + +@singleton +class Sync(object): + filetransfer = None + dbhelper = None + + sync_dir_config = {} + _observer = [] + _sync_paths = [] + _sync_sys = OsType.LINUX + _synced_files = [] + _need_sync_paths = {} + + def __init__(self): + self.init_config() + + def init_config(self): + self.dbhelper = DbHelper() + self.filetransfer = FileTransfer() + sync = Config().get_config('sync') + sync_paths = self.dbhelper.get_config_sync_paths() + if sync and sync_paths: + if sync.get('nas_sys') == "windows": + self._sync_sys = OsType.WINDOWS + self._sync_paths = sync_paths + self.init_sync_dirs() + + def init_sync_dirs(self): + """ + 初始化监控文件配置 + """ + self.sync_dir_config = {} + if self._sync_paths: + for sync_item in self._sync_paths: + if not sync_item: + continue + # ID + sync_id = sync_item.ID + # 启用标志 + enabled = True if sync_item.ENABLED else False + # 仅硬链接标志 + only_link = False if sync_item.RENAME else True + # 转移方式 + path_syncmode = ModuleConf.RMT_MODES.get(sync_item.MODE) + # 源目录|目的目录|未知目录 + monpath = sync_item.SOURCE + target_path = sync_item.DEST + unknown_path = sync_item.UNKNOWN + if target_path and unknown_path: + log.info("【Sync】读取到监控目录:%s,目的目录:%s,未识别目录:%s,转移方式:%s" % ( + monpath, target_path, unknown_path, path_syncmode.value)) + elif target_path: + log.info( + "【Sync】读取到监控目录:%s,目的目录:%s,转移方式:%s" % (monpath, target_path, path_syncmode.value)) + else: + log.info("【Sync】读取到监控目录:%s,转移方式:%s" % (monpath, path_syncmode.value)) + if not enabled: + log.info("【Sync】%s 不进行监控和同步:手动关闭" % monpath) + continue + if only_link: + log.info("【Sync】%s 不进行识别和重命名" % monpath) + if target_path and not os.path.exists(target_path): + log.info("【Sync】目的目录不存在,正在创建:%s" % target_path) + os.makedirs(target_path) + if unknown_path and not os.path.exists(unknown_path): + log.info("【Sync】未识别目录不存在,正在创建:%s" % unknown_path) + os.makedirs(unknown_path) + # 登记关系 + if os.path.exists(monpath): + self.sync_dir_config[monpath] = { + 'id': sync_id, + 'target': target_path, + 'unknown': unknown_path, + 'onlylink': only_link, + 'syncmod': path_syncmode + } + else: + log.error("【Sync】%s 目录不存在!" % monpath) + + def get_sync_dirs(self): + """ + 返回所有的同步监控目录 + """ + if not self.sync_dir_config: + return [] + return [os.path.normpath(key) for key in self.sync_dir_config.keys()] + + def file_change_handler(self, event, text, event_path): + """ + 处理文件变化 + :param event: 事件 + :param text: 事件描述 + :param event_path: 事件文件路径 + """ + if not event.is_directory: + # 文件发生变化 + try: + if not os.path.exists(event_path): + return + log.debug("【Sync】文件%s:%s" % (text, event_path)) + # 判断是否处理过了 + need_handler_flag = False + try: + lock.acquire() + if event_path not in self._synced_files: + self._synced_files.append(event_path) + need_handler_flag = True + finally: + lock.release() + if not need_handler_flag: + log.debug("【Sync】文件已处理过:%s" % event_path) + return + # 不是监控目录下的文件不处理 + is_monitor_file = False + for tpath in self.sync_dir_config.keys(): + if PathUtils.is_path_in_path(tpath, event_path): + is_monitor_file = True + break + if not is_monitor_file: + return + # 目的目录的子文件不处理 + for tpath in self.sync_dir_config.values(): + if not tpath: + continue + if PathUtils.is_path_in_path(tpath.get('target'), event_path): + return + if PathUtils.is_path_in_path(tpath.get('unknown'), event_path): + return + # 媒体库目录及子目录不处理 + if self.filetransfer.is_target_dir_path(event_path): + return + # 回收站及隐藏的文件不处理 + if PathUtils.is_invalid_path(event_path): + return + # 上级目录 + from_dir = os.path.dirname(event_path) + # 找到是哪个监控目录下的 + monitor_dir = event_path + is_root_path = False + for m_path in self.sync_dir_config.keys(): + if PathUtils.is_path_in_path(m_path, event_path): + monitor_dir = m_path + if os.path.normpath(m_path) == os.path.normpath(from_dir): + is_root_path = True + + # 查找目的目录 + target_dirs = self.sync_dir_config.get(monitor_dir) + target_path = target_dirs.get('target') + unknown_path = target_dirs.get('unknown') + onlylink = target_dirs.get('onlylink') + sync_mode = target_dirs.get('syncmod') + + # 只做硬链接,不做识别重命名 + if onlylink: + if self.dbhelper.is_sync_in_history(event_path, target_path): + return + log.info("【Sync】开始同步 %s" % event_path) + ret, msg = self.filetransfer.link_sync_file(src_path=monitor_dir, + in_file=event_path, + target_dir=target_path, + sync_transfer_mode=sync_mode) + if ret != 0: + log.warn("【Sync】%s 同步失败,错误码:%s" % (event_path, ret)) + elif not msg: + self.dbhelper.insert_sync_history(event_path, monitor_dir, target_path) + log.info("【Sync】%s 同步完成" % event_path) + # 识别转移 + else: + # 不是媒体文件不处理 + name = os.path.basename(event_path) + if not name: + return + if name.lower() != "index.bdmv": + ext = os.path.splitext(name)[-1] + if ext.lower() not in RMT_MEDIAEXT: + return + # 监控根目录下的文件发生变化时直接发走 + if is_root_path: + ret, ret_msg = self.filetransfer.transfer_media(in_from=SyncType.MON, + in_path=event_path, + target_dir=target_path, + unknown_dir=unknown_path, + rmt_mode=sync_mode) + if not ret: + log.warn("【Sync】%s 转移失败:%s" % (event_path, ret_msg)) + else: + try: + lock.acquire() + if self._need_sync_paths.get(from_dir): + files = self._need_sync_paths[from_dir].get('files') + if not files: + files = [event_path] + else: + if event_path not in files: + files.append(event_path) + else: + return + self._need_sync_paths[from_dir].update({'files': files}) + else: + self._need_sync_paths[from_dir] = {'target': target_path, + 'unknown': unknown_path, + 'syncmod': sync_mode, + 'files': [event_path]} + finally: + lock.release() + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("【Sync】发生错误:%s - %s" % (str(e), traceback.format_exc())) + + def transfer_mon_files(self): + """ + 批量转移文件,由定时服务定期调用执行 + """ + try: + lock.acquire() + finished_paths = [] + for path in list(self._need_sync_paths): + if not PathUtils.is_invalid_path(path) and os.path.exists(path): + log.info("【Sync】开始转移监控目录文件...") + target_info = self._need_sync_paths.get(path) + bluray_dir = PathUtils.get_bluray_dir(path) + if not bluray_dir: + src_path = path + files = target_info.get('files') + else: + src_path = bluray_dir + files = [] + if src_path not in finished_paths: + finished_paths.append(src_path) + else: + continue + target_path = target_info.get('target') + unknown_path = target_info.get('unknown') + sync_mode = target_info.get('syncmod') + # 判断是否根目录 + is_root_path = False + for m_path in self.sync_dir_config.keys(): + if os.path.normpath(m_path) == os.path.normpath(src_path): + is_root_path = True + ret, ret_msg = self.filetransfer.transfer_media(in_from=SyncType.MON, + in_path=src_path, + files=files, + target_dir=target_path, + unknown_dir=unknown_path, + rmt_mode=sync_mode, + root_path=is_root_path) + if not ret: + log.warn("【Sync】%s转移失败:%s" % (path, ret_msg)) + self._need_sync_paths.pop(path) + finally: + lock.release() + + def run_service(self): + """ + 启动监控服务 + """ + self._observer = [] + for monpath in self.sync_dir_config.keys(): + if monpath and os.path.exists(monpath): + try: + if self._sync_sys == OsType.WINDOWS: + # 考虑到windows的docker需要直接指定才能生效(修改配置文件为windows) + observer = PollingObserver(timeout=10) + else: + # 内部处理系统操作类型选择最优解 + observer = Observer(timeout=10) + self._observer.append(observer) + observer.schedule(FileMonitorHandler(monpath, self), path=monpath, recursive=True) + observer.daemon = True + observer.start() + log.info("%s 的监控服务启动" % monpath) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error("%s 启动目录监控失败:%s" % (monpath, str(e))) + + def stop_service(self): + """ + 关闭监控服务 + """ + if self._observer: + for observer in self._observer: + observer.stop() + self._observer = [] + + def transfer_all_sync(self, sid=None): + """ + 全量转移Sync目录下的文件,WEB界面点击目录同步时获发 + """ + for monpath, target_dirs in self.sync_dir_config.items(): + if not monpath: + continue + if sid and sid != target_dirs.get('id'): + continue + target_path = target_dirs.get('target') + unknown_path = target_dirs.get('unknown') + onlylink = target_dirs.get('onlylink') + sync_mode = target_dirs.get('syncmod') + # 只做硬链接,不做识别重命名 + if onlylink: + for link_file in PathUtils.get_dir_files(monpath): + if self.dbhelper.is_sync_in_history(link_file, target_path): + continue + log.info("【Sync】开始同步 %s" % link_file) + ret, msg = self.filetransfer.link_sync_file(src_path=monpath, + in_file=link_file, + target_dir=target_path, + sync_transfer_mode=sync_mode) + if ret != 0: + log.warn("【Sync】%s 同步失败,错误码:%s" % (link_file, ret)) + elif not msg: + self.dbhelper.insert_sync_history(link_file, monpath, target_path) + log.info("【Sync】%s 同步完成" % link_file) + else: + for path in PathUtils.get_dir_level1_medias(monpath, RMT_MEDIAEXT): + if PathUtils.is_invalid_path(path): + continue + ret, ret_msg = self.filetransfer.transfer_media(in_from=SyncType.MON, + in_path=path, + target_dir=target_path, + unknown_dir=unknown_path, + rmt_mode=sync_mode) + if not ret: + log.error("【Sync】%s 处理失败:%s" % (monpath, ret_msg)) + + +def run_monitor(): + """ + 启动监控 + """ + try: + Sync().run_service() + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("启动目录同步服务失败:%s" % str(err)) + + +def stop_monitor(): + """ + 停止监控 + """ + try: + Sync().stop_service() + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("停止目录同步服务失败:%s" % str(err)) + + +def restart_monitor(): + """ + 重启监控 + """ + stop_monitor() + run_monitor() diff --git a/app/torrentremover.py b/app/torrentremover.py new file mode 100644 index 0000000..d33686a --- /dev/null +++ b/app/torrentremover.py @@ -0,0 +1,305 @@ +import json +from threading import Lock + +from apscheduler.schedulers.background import BackgroundScheduler + +import log +from app.conf import ModuleConf +from app.downloader import Downloader +from app.helper import DbHelper +from app.message import Message +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from config import Config + +lock = Lock() + + +@singleton +class TorrentRemover(object): + message = None + downloader = None + dbhelper = None + + _scheduler = None + _remove_tasks = {} + + def __init__(self): + self.init_config() + + def init_config(self): + self.message = Message() + self.downloader = Downloader() + self.dbhelper = DbHelper() + # 移出现有任务 + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + # 读取任务任务列表 + removetasks = self.dbhelper.get_torrent_remove_tasks() + self._remove_tasks = {} + for task in removetasks: + config = task.CONFIG + self._remove_tasks[str(task.ID)] = { + "id": task.ID, + "name": task.NAME, + "downloader": task.DOWNLOADER, + "onlynastool": task.ONLYNASTOOL, + "samedata": task.SAMEDATA, + "action": task.ACTION, + "config": json.loads(config) if config else {}, + "interval": task.INTERVAL, + "enabled": task.ENABLED, + } + if not self._remove_tasks: + return + # 启动删种任务 + self._scheduler = BackgroundScheduler(timezone=Config().get_timezone()) + remove_flag = False + for task in self._remove_tasks.values(): + if task.get("enabled") and task.get("interval") and task.get("config"): + remove_flag = True + self._scheduler.add_job(func=self.auto_remove_torrents, + args=[task.get("id")], + trigger='interval', + seconds=int(task.get("interval")) * 60) + if remove_flag: + self._scheduler.print_jobs() + self._scheduler.start() + log.info("自动删种服务启动") + + def get_torrent_remove_tasks(self, taskid=None): + """ + 获取删种任务详细信息 + """ + if taskid: + task = self._remove_tasks.get(str(taskid)) + return task if task else {} + return self._remove_tasks + + def auto_remove_torrents(self, taskids=None): + """ + 处理自动删种任务,由定时服务调用 + :param taskids: 自动删种任务的ID + """ + # 获取自动删种任务 + tasks = [] + # 如果没有指定任务ID,则处理所有启用任务 + if not taskids: + for task in self._remove_tasks.values(): + if task.get("enabled") and task.get("interval") and task.get("config"): + tasks.append(task) + # 如果指定任务id,则处理指定任务无论是否启用 + elif isinstance(taskids, list): + for taskid in taskids: + task = self._remove_tasks.get(str(taskid)) + if task: + tasks.append(task) + else: + task = self._remove_tasks.get(str(taskids)) + tasks = [task] if task else [] + if not tasks: + return + for task in tasks: + try: + lock.acquire() + # 获取需删除种子列表 + downloader_type = ModuleConf.TORRENTREMOVER_DICT.get(task.get("downloader")).get("downloader_type") + task.get("config")["samedata"] = task.get("samedata") + task.get("config")["onlynastool"] = task.get("onlynastool") + torrents = self.downloader.get_remove_torrents( + downloader=downloader_type, + config=task.get("config") + ) + log.info(f"【TorrentRemover】自动删种任务:{task.get('name')} 获取符合处理条件种子数 {len(torrents)}") + title = f"自动删种任务:{task.get('name')}" + text = "" + if task.get("action") == 1: + text = f"共暂停{len(torrents)}个种子" + for torrent in torrents: + name = torrent.get("name") + site = torrent.get("site") + size = round(torrent.get("size")/1021/1024/1024, 3) + text_item = f"{name} 来自站点:{site} 大小:{size} GB" + log.info(f"【TorrentRemover】暂停种子:{text_item}") + text = f"{text}\n{text_item}" + # 暂停种子 + self.downloader.stop_torrents(downloader=downloader_type, + ids=[torrent.get("id")]) + elif task.get("action") == 2: + text = f"共删除{len(torrents)}个种子" + for torrent in torrents: + name = torrent.get("name") + site = torrent.get("site") + size = round(torrent.get("size") / 1021 / 1024 / 1024, 3) + text_item = f"{name} 来自站点:{site} 大小:{size} GB" + log.info(f"【TorrentRemover】删除种子:{text_item}") + text = f"{text}\n{text_item}" + # 删除种子 + self.downloader.delete_torrents(downloader=downloader_type, + delete_file=False, + ids=[torrent.get("id")]) + elif task.get("action") == 3: + text = f"共删除{len(torrents)}个种子(及文件)" + for torrent in torrents: + name = torrent.get("name") + site = torrent.get("site") + size = round(torrent.get("size") / 1021 / 1024 / 1024, 3) + text_item = f"{name} 来自站点:{site} 大小:{size} GB" + log.info(f"【TorrentRemover】删除种子及文件:{text_item}") + text = f"{text}\n{text_item}" + # 删除种子 + self.downloader.delete_torrents(downloader=downloader_type, + delete_file=True, + ids=[torrent.get("id")]) + if torrents and title and text: + self.message.send_brushtask_remove_message(title=title, text=text) + except Exception as e: + ExceptionUtils.exception_traceback(e) + log.error(f"【TorrentRemover】自动删种任务:{task.get('name')}异常:{str(e)}") + finally: + lock.release() + + def update_torrent_remove_task(self, data): + """ + 更新自动删种任务 + """ + tid = data.get("tid") + name = data.get("name") + if not name: + return False, "名称参数不合法" + action = data.get("action") + if not str(action).isdigit() or int(action) not in [1, 2, 3]: + return False, "动作参数不合法" + else: + action = int(action) + interval = data.get("interval") + if not str(interval).isdigit(): + return False, "运行间隔参数不合法" + else: + interval = int(interval) + enabled = data.get("enabled") + if not str(enabled).isdigit() or int(enabled) not in [0, 1]: + return False, "状态参数不合法" + else: + enabled = int(enabled) + samedata = data.get("samedata") + if not str(enabled).isdigit() or int(samedata) not in [0, 1]: + return False, "处理辅种参数不合法" + else: + samedata = int(samedata) + onlynastool = data.get("onlynastool") + if not str(enabled).isdigit() or int(onlynastool) not in [0, 1]: + return False, "仅处理NASTOOL添加种子参数不合法" + else: + onlynastool = int(onlynastool) + ratio = data.get("ratio") or 0 + if not str(ratio).replace(".", "").isdigit(): + return False, "分享率参数不合法" + else: + ratio = round(float(ratio), 2) + seeding_time = data.get("seeding_time") or 0 + if not str(seeding_time).isdigit(): + return False, "做种时间参数不合法" + else: + seeding_time = int(seeding_time) + upload_avs = data.get("upload_avs") or 0 + if not str(upload_avs).isdigit(): + return False, "平均上传速度参数不合法" + else: + upload_avs = int(upload_avs) + size = data.get("size") + size = str(size).split("-") if size else [] + if size and (len(size) != 2 or not str(size[0]).isdigit() or not str(size[-1]).isdigit()): + return False, "种子大小参数不合法" + else: + size = [int(size[0]), int(size[-1])] if size else [] + tags = data.get("tags") + tags = tags.split(";") if tags else [] + tags = [tag for tag in tags if tag] + savepath_key = data.get("savepath_key") + tracker_key = data.get("tracker_key") + downloader = data.get("downloader") + if downloader not in ModuleConf.TORRENTREMOVER_DICT.keys(): + return False, "下载器参数不合法" + if downloader == "Qb": + qb_state = data.get("qb_state") + qb_state = qb_state.split(";") if qb_state else [] + qb_state = [state for state in qb_state if state] + if qb_state: + for qb_state_item in qb_state: + if qb_state_item not in ModuleConf.TORRENTREMOVER_DICT.get("Qb").get("torrent_state").keys(): + return False, "种子状态参数不合法" + qb_category = data.get("qb_category") + qb_category = qb_category.split(";") if qb_category else [] + qb_category = [category for category in qb_category if category] + tr_state = [] + tr_error_key = "" + else: + qb_state = [] + qb_category = [] + tr_state = data.get("tr_state") + tr_state = tr_state.split(";") if tr_state else [] + tr_state = [state for state in tr_state if state] + if tr_state: + for tr_state_item in tr_state: + if tr_state_item not in ModuleConf.TORRENTREMOVER_DICT.get("Tr").get("torrent_state").keys(): + return False, "种子状态参数不合法" + tr_error_key = data.get("tr_error_key") + config = { + "ratio": ratio, + "seeding_time": seeding_time, + "upload_avs": upload_avs, + "size": size, + "tags": tags, + "savepath_key": savepath_key, + "tracker_key": tracker_key, + "qb_state": qb_state, + "qb_category": qb_category, + "tr_state": tr_state, + "tr_error_key": tr_error_key, + } + if tid: + self.dbhelper.delete_torrent_remove_task(tid=tid) + self.dbhelper.insert_torrent_remove_task( + name=name, + action=action, + interval=interval, + enabled=enabled, + samedata=samedata, + onlynastool=onlynastool, + downloader=downloader, + config=config, + ) + return True, "更新成功" + + def delete_torrent_remove_task(self, taskid=None): + """ + 删除自动删种任务 + """ + if not taskid: + return False + else: + self.dbhelper.delete_torrent_remove_task(tid=taskid) + return True + + def get_remove_torrents(self, taskid): + """ + 获取满足自动删种任务的种子 + """ + task = self._remove_tasks.get(str(taskid)) + if not task: + return False, [] + else: + task.get("config")["samedata"] = task.get("samedata") + task.get("config")["onlynastool"] = task.get("onlynastool") + torrents = self.downloader.get_remove_torrents( + downloader=ModuleConf.TORRENTREMOVER_DICT.get(task.get("downloader")).get("downloader_type"), + config=task.get("config") + ) + return True, torrents diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..addef48 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,13 @@ +from .dom_utils import DomUtils +from .episode_format import EpisodeFormat +from .http_utils import RequestUtils +from .json_utils import JsonUtils +from .number_utils import NumberUtils +from .path_utils import PathUtils +from .string_utils import StringUtils +from .system_utils import SystemUtils +from .tokens import Tokens +from .torrent import Torrent +from .cache_manager import cacheman, TokenCache, ConfigLoadCache +from .exception_utils import ExceptionUtils +from .rsstitle_utils import RssTitleUtils diff --git a/app/utils/cache_manager.py b/app/utils/cache_manager.py new file mode 100644 index 0000000..fdf140b --- /dev/null +++ b/app/utils/cache_manager.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import time + +from cacheout import CacheManager, LRUCache, Cache + +CACHES = { + "tmdb_supply": {'maxsize': 200} +} + +cacheman = CacheManager(CACHES, cache_class=LRUCache) + +TokenCache = Cache(maxsize=256, ttl=4*3600, timer=time.time, default=None) + +ConfigLoadCache = Cache(maxsize=1, ttl=10, timer=time.time, default=None) diff --git a/app/utils/commons.py b/app/utils/commons.py new file mode 100644 index 0000000..fc00c6b --- /dev/null +++ b/app/utils/commons.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import threading + +# 线程锁 +lock = threading.RLock() + +# 全局实例 +INSTANCES = {} + + +# 单例模式注解 +def singleton(cls): + # 创建字典用来保存类的实例对象 + global INSTANCES + + def _singleton(*args, **kwargs): + # 先判断这个类有没有对象 + if cls not in INSTANCES: + with lock: + if cls not in INSTANCES: + INSTANCES[cls] = cls(*args, **kwargs) + pass + # 将实例对象返回 + return INSTANCES[cls] + + return _singleton diff --git a/app/utils/dom_utils.py b/app/utils/dom_utils.py new file mode 100644 index 0000000..2e9070a --- /dev/null +++ b/app/utils/dom_utils.py @@ -0,0 +1,30 @@ +class DomUtils: + + @staticmethod + def tag_value(tag_item, tag_name, attname="", default=None): + """ + 解析XML标签值 + """ + tagNames = tag_item.getElementsByTagName(tag_name) + if tagNames: + if attname: + attvalue = tagNames[0].getAttribute(attname) + if attvalue: + return attvalue + else: + firstChild = tagNames[0].firstChild + if firstChild: + return firstChild.data + return default + + @staticmethod + def add_node(doc, parent, name, value=None): + """ + 添加一个DOM节点 + """ + node = doc.createElement(name) + parent.appendChild(node) + if value is not None: + text = doc.createTextNode(str(value)) + node.appendChild(text) + return node diff --git a/app/utils/episode_format.py b/app/utils/episode_format.py new file mode 100644 index 0000000..031f194 --- /dev/null +++ b/app/utils/episode_format.py @@ -0,0 +1,85 @@ +import re +import parse +from config import SPLIT_CHARS + + +class EpisodeFormat(object): + _key = "" + + def __init__(self, eformat, details: str = None, offset=None, key="ep"): + self._format = eformat + self._start_ep = None + self._end_ep = None + if details: + if re.compile("\\d{1,4}-\\d{1,4}").match(details): + self._start_ep = details + self._end_ep = details + else: + tmp = details.split(",") + if len(tmp) > 1: + self._start_ep = int(tmp[0]) + self._end_ep = int(tmp[0]) if int(tmp[0]) > int(tmp[1]) else int(tmp[1]) + else: + self._start_ep = self._end_ep = int(tmp[0]) + self.__offset = int(offset) if offset else 0 + self._key = key + + @property + def format(self): + return self._format + + @property + def start_ep(self): + return self._start_ep + + @property + def end_ep(self): + return self._end_ep + + @property + def offset(self): + return self.__offset + + def match(self, file: str): + if not self._format: + return True + s, e = self.__handle_single(file) + if not s: + return False + if self._start_ep is None: + return True + if self._start_ep <= s <= self._end_ep: + return True + return False + + def split_episode(self, file_name): + # 指定的具体集数,直接返回 + if self._start_ep is not None and self._start_ep == self._end_ep: + if isinstance(self._start_ep, str): + s, e = self._start_ep.split("-") + if int(s) == int(e): + return int(s) + self.__offset, None + return int(s) + self.__offset, int(e) + self.__offset + return self._start_ep + self.__offset, None + if not self._format: + return None, None + s, e = self.__handle_single(file_name) + return s + self.__offset if s is not None else None, e + self.__offset if e is not None else None + + def __handle_single(self, file: str): + if not self._format: + return None, None + ret = parse.parse(self._format, file) + if not ret or not ret.__contains__(self._key): + return None, None + episodes = ret.__getitem__(self._key) + if not re.compile(r"^(EP)?(\d{1,4})(-(EP)?(\d{1,4}))?$", re.IGNORECASE).match(episodes): + return None, None + episode_splits = list(filter(lambda x: re.compile(r'[a-zA-Z]*\d{1,4}', re.IGNORECASE).match(x), + re.split(r'%s' % SPLIT_CHARS, episodes))) + if len(episode_splits) == 1: + return int(re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[0])), None + else: + return int(re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[0])), int( + re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[1])) + diff --git a/app/utils/exception_utils.py b/app/utils/exception_utils.py new file mode 100644 index 0000000..fa5e2f3 --- /dev/null +++ b/app/utils/exception_utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +import traceback + + +class ExceptionUtils: + @classmethod + def exception_traceback(cls, e): + print(f"\nException: {str(e)}\nCallstack:\n{traceback.format_exc()}\n") diff --git a/app/utils/http_utils.py b/app/utils/http_utils.py new file mode 100644 index 0000000..f5d5b64 --- /dev/null +++ b/app/utils/http_utils.py @@ -0,0 +1,164 @@ +import requests +import urllib3 +from urllib3.exceptions import InsecureRequestWarning +from config import Config + +urllib3.disable_warnings(InsecureRequestWarning) + + +class RequestUtils: + _headers = None + _cookies = None + _proxies = None + _timeout = 20 + _session = None + + def __init__(self, + headers=None, + cookies=None, + proxies=False, + session=None, + timeout=None, + referer=None, + content_type=None): + if not content_type: + content_type = "application/x-www-form-urlencoded; charset=UTF-8" + if headers: + if isinstance(headers, str): + self._headers = { + "Content-Type": content_type, + "User-Agent": f"{headers}" + } + else: + self._headers = headers + else: + self._headers = { + "Content-Type": content_type, + "User-Agent": Config().get_ua() + } + if referer: + self._headers.update({ + "referer": referer + }) + if cookies: + if isinstance(cookies, str): + self._cookies = self.cookie_parse(cookies) + else: + self._cookies = cookies + if proxies: + self._proxies = proxies + if session: + self._session = session + if timeout: + self._timeout = timeout + + def post(self, url, params=None, json=None): + if json is None: + json = {} + try: + if self._session: + return self._session.post(url, + data=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + timeout=self._timeout, + json=json) + else: + return requests.post(url, + data=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + timeout=self._timeout, + json=json) + except requests.exceptions.RequestException: + return None + + def get(self, url, params=None): + try: + if self._session: + r = self._session.get(url, + verify=False, + headers=self._headers, + proxies=self._proxies, + timeout=self._timeout, + params=params) + else: + r = requests.get(url, + verify=False, + headers=self._headers, + proxies=self._proxies, + timeout=self._timeout, + params=params) + return str(r.content, 'utf-8') + except requests.exceptions.RequestException: + return None + + def get_res(self, url, params=None, allow_redirects=True): + try: + if self._session: + return self._session.get(url, + params=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + cookies=self._cookies, + timeout=self._timeout, + allow_redirects=allow_redirects) + else: + return requests.get(url, + params=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + cookies=self._cookies, + timeout=self._timeout, + allow_redirects=allow_redirects) + except requests.exceptions.RequestException: + return None + + def post_res(self, url, params=None, allow_redirects=True, files=None, json=None): + try: + if self._session: + return self._session.post(url, + data=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + cookies=self._cookies, + timeout=self._timeout, + allow_redirects=allow_redirects, + files=files, + json=json) + else: + return requests.post(url, + data=params, + verify=False, + headers=self._headers, + proxies=self._proxies, + cookies=self._cookies, + timeout=self._timeout, + allow_redirects=allow_redirects, + files=files, + json=json) + except requests.exceptions.RequestException: + return None + + @staticmethod + def cookie_parse(cookies_str, array=False): + if not cookies_str: + return {} + cookie_dict = {} + cookies = cookies_str.split(';') + for cookie in cookies: + cstr = cookie.split('=') + if len(cstr) > 1: + cookie_dict[cstr[0].strip()] = cstr[1].strip() + if array: + cookiesList = [] + for cookieName, cookieValue in cookie_dict.items(): + cookies = {'name': cookieName, 'value': cookieValue} + cookiesList.append(cookies) + return cookiesList + return cookie_dict diff --git a/app/utils/json_utils.py b/app/utils/json_utils.py new file mode 100644 index 0000000..ccfe2e5 --- /dev/null +++ b/app/utils/json_utils.py @@ -0,0 +1,24 @@ +import json +from enum import Enum + + +class JsonUtils: + + @staticmethod + def json_serializable(obj): + """ + 将普通对象转化为支持json序列化的对象 + @param obj: 待转化的对象 + @return: 支持json序列化的对象 + """ + + def _try(o): + if isinstance(o, Enum): + return o.value + try: + return o.__dict__ + except Exception as err: + print(str(err)) + return str(o) + + return json.loads(json.dumps(obj, default=lambda o: _try(o))) diff --git a/app/utils/number_utils.py b/app/utils/number_utils.py new file mode 100644 index 0000000..693fc14 --- /dev/null +++ b/app/utils/number_utils.py @@ -0,0 +1,12 @@ +class NumberUtils: + + @staticmethod + def max_ele(a, b): + """ + 返回非空最大值 + """ + if not a: + return b + if not b: + return a + return max(int(a), int(b)) diff --git a/app/utils/path_utils.py b/app/utils/path_utils.py new file mode 100644 index 0000000..ce85c90 --- /dev/null +++ b/app/utils/path_utils.py @@ -0,0 +1,155 @@ +import os + + +class PathUtils: + + @staticmethod + def get_dir_files(in_path, exts="", filesize=0, episode_format=None): + """ + 获得目录下的媒体文件列表List ,按后缀、大小、格式过滤 + """ + if not in_path: + return [] + if not os.path.exists(in_path): + return [] + ret_list = [] + if os.path.isdir(in_path): + for root, dirs, files in os.walk(in_path): + for file in files: + cur_path = os.path.join(root, file) + # 检查路径是否合法 + if PathUtils.is_invalid_path(cur_path): + continue + # 检查格式匹配 + if episode_format and not episode_format.match(file): + continue + # 检查后缀 + if exts and os.path.splitext(file)[-1].lower() not in exts: + continue + # 检查文件大小 + if filesize and os.path.getsize(cur_path) < filesize: + continue + # 命中 + if cur_path not in ret_list: + ret_list.append(cur_path) + else: + # 检查路径是否合法 + if PathUtils.is_invalid_path(in_path): + return [] + # 检查后缀 + if exts and os.path.splitext(in_path)[-1].lower() not in exts: + return [] + # 检查格式 + if episode_format and not episode_format.match(os.path.basename(in_path)): + return [] + # 检查文件大小 + if filesize and os.path.getsize(in_path) < filesize: + return [] + ret_list.append(in_path) + return ret_list + + @staticmethod + def get_dir_level1_files(in_path, exts=""): + """ + 查询目录下的文件(只查询一级) + """ + ret_list = [] + if not os.path.exists(in_path): + return [] + for file in os.listdir(in_path): + path = os.path.join(in_path, file) + if os.path.isfile(path): + if not exts or os.path.splitext(file)[-1].lower() in exts: + ret_list.append(path) + return ret_list + + @staticmethod + def get_dir_level1_medias(in_path, exts=""): + """ + 根据后缀,返回目录下所有的文件及文件夹列表(只查询一级) + """ + ret_list = [] + if not os.path.exists(in_path): + return [] + if os.path.isdir(in_path): + for file in os.listdir(in_path): + path = os.path.join(in_path, file) + if os.path.isfile(path): + if not exts or os.path.splitext(file)[-1].lower() in exts: + ret_list.append(path) + else: + ret_list.append(path) + else: + ret_list.append(in_path) + return ret_list + + @staticmethod + def is_invalid_path(path): + """ + 判断是否不能处理的路径 + """ + if not path: + return True + if path.find('/@Recycle/') != -1 or path.find('/#recycle/') != -1 or path.find('/.') != -1 or path.find( + '/@eaDir') != -1: + return True + return False + + @staticmethod + def is_path_in_path(path1, path2): + """ + 判断两个路径是否包含关系 path1 in path2 + """ + if not path1 or not path2: + return False + path1 = os.path.normpath(path1) + path2 = os.path.normpath(path2) + if path1 == path2: + return True + path = os.path.dirname(path2) + while True: + if path == path1: + return True + path = os.path.dirname(path) + if path == os.path.dirname(path): + break + return False + + @staticmethod + def get_bluray_dir(path): + """ + 判断是否蓝光原盘目录,是则返回原盘的根目录,否则返回空 + """ + if not path or not os.path.exists(path): + return None + if os.path.isdir(path): + if os.path.exists(os.path.join(path, "BDMV", "index.bdmv")): + return path + elif os.path.normpath(path).endswith("BDMV") \ + and os.path.exists(os.path.join(path, "index.bdmv")): + return os.path.dirname(path) + elif os.path.normpath(path).endswith("STREAM") \ + and os.path.exists(os.path.join(os.path.dirname(path), "index.bdmv")): + return PathUtils.get_parent_paths(path, 2) + else: + # 电视剧原盘下会存在多个目录形如:Spider Man 2021/DIsc1, Spider Man 2021/Disc2 + for level1 in PathUtils.get_dir_level1_medias(path): + if os.path.exists(os.path.join(level1, "BDMV", "index.bdmv")): + return path + return None + else: + if str(os.path.splitext(path)[-1]).lower() in [".m2ts", ".ts"] \ + and os.path.normpath(os.path.dirname(path)).endswith("STREAM") \ + and os.path.exists(os.path.join(PathUtils.get_parent_paths(path, 2), "index.bdmv")): + return PathUtils.get_parent_paths(path, 3) + else: + return None + + @staticmethod + def get_parent_paths(path, level: int = 1): + """ + 获取父目录路径,level为向上查找的层数 + """ + for lv in range(0, level): + path = os.path.dirname(path) + return path diff --git a/app/utils/rsstitle_utils.py b/app/utils/rsstitle_utils.py new file mode 100644 index 0000000..dc2ad1f --- /dev/null +++ b/app/utils/rsstitle_utils.py @@ -0,0 +1,30 @@ +import re + +from app.utils.exception_utils import ExceptionUtils + + +class RssTitleUtils: + + @staticmethod + def keepfriends_title(title): + """ + 处理pt.keepfrds.com的RSS标题 + """ + if not title: + return "" + try: + title_search = re.search(r"\[(.*)]", title, re.IGNORECASE) + if title_search: + if title_search.span()[0] == 0: + title_all = re.findall(r"\[(.*?)]", title, re.IGNORECASE) + if title_all and len(title_all) > 1: + torrent_name = title_all[-1] + torrent_desc = title.replace(f"[{torrent_name}]", "").strip() + title = "%s %s" % (torrent_name, torrent_desc) + else: + torrent_name = title_search.group(1) + torrent_desc = title.replace(title_search.group(), "").strip() + title = "%s %s" % (torrent_name, torrent_desc) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return title diff --git a/app/utils/string_utils.py b/app/utils/string_utils.py new file mode 100644 index 0000000..ad27d8d --- /dev/null +++ b/app/utils/string_utils.py @@ -0,0 +1,440 @@ +import bisect +import datetime +import hashlib +import random +import re +from urllib import parse + +import dateparser +import dateutil.parser + +import cn2an +from app.utils.exception_utils import ExceptionUtils +from app.utils.types import MediaType + + +class StringUtils: + + @staticmethod + def num_filesize(text): + """ + 将文件大小文本转化为字节 + """ + if not text: + return 0 + if not isinstance(text, str): + text = str(text) + if text.isdigit(): + return int(text) + text = text.replace(",", "").replace(" ", "").upper() + size = re.sub(r"[KMGTPI]*B?", "", text, flags=re.IGNORECASE) + try: + size = float(size) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return 0 + if text.find("PB") != -1 or text.find("PIB") != -1: + size *= 1024 ** 5 + elif text.find("TB") != -1 or text.find("TIB") != -1: + size *= 1024 ** 4 + elif text.find("GB") != -1 or text.find("GIB") != -1: + size *= 1024 ** 3 + elif text.find("MB") != -1 or text.find("MIB") != -1: + size *= 1024 ** 2 + elif text.find("KB") != -1 or text.find("KIB") != -1: + size *= 1024 + return round(size) + + @staticmethod + def str_timelong(time_sec): + """ + 将数字转换为时间描述 + """ + if not isinstance(time_sec, int) or not isinstance(time_sec, float): + try: + time_sec = float(time_sec) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return "" + d = [(0, '秒'), (60 - 1, '分'), (3600 - 1, '小时'), (86400 - 1, '天')] + s = [x[0] for x in d] + index = bisect.bisect_left(s, time_sec) - 1 + if index == -1: + return str(time_sec) + else: + b, u = d[index] + return str(round(time_sec / (b + 1))) + u + + @staticmethod + def is_chinese(word): + """ + 判断是否含有中文 + """ + if isinstance(word, list): + word = " ".join(word) + chn = re.compile(r'[\u4e00-\u9fff]') + if chn.search(word): + return True + else: + return False + + @staticmethod + def is_japanese(word): + jap = re.compile(r'[\u3040-\u309F\u30A0-\u30FF]') + if jap.search(word): + return True + else: + return False + + @staticmethod + def is_korean(word): + kor = re.compile(r'[\uAC00-\uD7FF]') + if kor.search(word): + return True + else: + return False + + @staticmethod + def is_all_chinese(word): + """ + 判断是否全是中文 + """ + for ch in word: + if ch == ' ': + continue + if '\u4e00' <= ch <= '\u9fff': + continue + else: + return False + return True + + @staticmethod + def xstr(s): + """ + 字符串None输出为空 + """ + return s if s else '' + + @staticmethod + def str_sql(in_str): + """ + 转化SQL字符 + """ + return "" if not in_str else str(in_str) + + @staticmethod + def str_int(text): + """ + web字符串转int + :param text: + :return: + """ + int_val = 0 + try: + int_val = int(text.strip().replace(',', '')) + except Exception as e: + ExceptionUtils.exception_traceback(e) + + return int_val + + @staticmethod + def str_float(text): + """ + web字符串转float + :param text: + :return: + """ + float_val = 0.0 + try: + float_val = float(text.strip().replace(',', '')) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return float_val + + @staticmethod + def handler_special_chars(text, replace_word="", allow_space=False): + """ + 忽略特殊字符 + """ + # 需要忽略的特殊字符 + CONVERT_EMPTY_CHARS = r"[、.。,,·::;;!!'’\"“”()()\[\]【】「」\-——\+\|\\_/&#~~]" + if not text: + return text + if not isinstance(text, list): + text = re.sub(r"[\u200B-\u200D\uFEFF]", + "", + re.sub(r"%s" % CONVERT_EMPTY_CHARS, replace_word, text), + flags=re.IGNORECASE) + if not allow_space: + return re.sub(r"\s+", "", text) + else: + return re.sub(r"\s+", " ", text).strip() + else: + return [StringUtils.handler_special_chars(x) for x in text] + + @staticmethod + def str_filesize(size, pre=2): + """ + 将字节计算为文件大小描述(带单位的格式化后返回) + """ + if not size: + return size + size = re.sub(r"\s|B|iB", "", str(size), re.I) + if size.replace(".", "").isdigit(): + try: + size = float(size) + d = [(1024 - 1, 'K'), (1024 ** 2 - 1, 'M'), (1024 ** 3 - 1, 'G'), (1024 ** 4 - 1, 'T')] + s = [x[0] for x in d] + index = bisect.bisect_left(s, size) - 1 + if index == -1: + return str(size) + "B" + else: + b, u = d[index] + return str(round(size / (b + 1), pre)) + u + except Exception as e: + ExceptionUtils.exception_traceback(e) + return "" + if re.findall(r"[KMGTP]", size, re.I): + return size + else: + return size + "B" + + @staticmethod + def url_equal(url1, url2): + """ + 比较两个地址是否为同一个网站 + """ + if not url1 or not url2: + return False + if url1.startswith("http"): + url1 = parse.urlparse(url1).netloc + if url2.startswith("http"): + url2 = parse.urlparse(url2).netloc + if url1.replace("www.", "") == url2.replace("www.", ""): + return True + return False + + @staticmethod + def get_url_netloc(url): + """ + 获取URL的协议和域名部分 + """ + if not url: + return "", "" + if not url.startswith("http"): + return "http", url + addr = parse.urlparse(url) + return addr.scheme, addr.netloc + + @staticmethod + def get_url_domain(url): + """ + 获取URL的域名部分,不含WWW和HTTP + """ + if not url: + return "" + _, netloc = StringUtils.get_url_netloc(url) + if netloc: + return netloc.lower().replace("www.", "") + return "" + + @staticmethod + def get_base_url(url): + """ + 获取URL根地址 + """ + if not url: + return "" + scheme, netloc = StringUtils.get_url_netloc(url) + return f"{scheme}://{netloc}" + + @staticmethod + def clear_file_name(name): + if not name: + return None + return re.sub(r"[*?\\/\"<>~]", "", name, flags=re.IGNORECASE).replace(":", ":") + + @staticmethod + def get_keyword_from_string(content): + """ + 从检索关键字中拆分中年份、季、集、类型 + """ + if not content: + return None, None, None, None, None + # 去掉查询中的电影或电视剧关键字 + if re.search(r'^电视剧|\s+电视剧|^动漫|\s+动漫', content): + mtype = MediaType.TV + else: + mtype = None + content = re.sub(r'^电影|^电视剧|^动漫|\s+电影|\s+电视剧|\s+动漫', '', content).strip() + # 稍微切一下剧集吧 + season_num = None + episode_num = None + year = None + season_re = re.search(r"第\s*([0-9一二三四五六七八九十]+)\s*季", content, re.IGNORECASE) + if season_re: + mtype = MediaType.TV + season_num = int(cn2an.cn2an(season_re.group(1), mode='smart')) + episode_re = re.search(r"第\s*([0-9一二三四五六七八九十]+)\s*集", content, re.IGNORECASE) + if episode_re: + mtype = MediaType.TV + episode_num = int(cn2an.cn2an(episode_re.group(1), mode='smart')) + if episode_num and not season_num: + season_num = 1 + year_re = re.search(r"[\s(]+(\d{4})[\s)]*", content) + if year_re: + year = year_re.group(1) + key_word = re.sub( + r'第\s*[0-9一二三四五六七八九十]+\s*季|第\s*[0-9一二三四五六七八九十]+\s*集|[\s(]+(\d{4})[\s)]*', '', + content, + flags=re.IGNORECASE).strip() + if key_word: + key_word = re.sub(r'\s+', ' ', key_word) + if not key_word: + key_word = year + + return mtype, key_word, season_num, episode_num, year, content + + @staticmethod + def generate_random_str(randomlength=16): + """ + 生成一个指定长度的随机字符串 + """ + random_str = '' + base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length)] + return random_str + + @staticmethod + def get_time_stamp(date): + tempsTime = None + try: + tempsTime = dateutil.parser.parse(date) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return tempsTime + + @staticmethod + def unify_datetime_str(datetime_str): + """ + 日期时间格式化 统一转成 2020-10-14 07:48:04 这种格式 + # 场景1: 带有时区的日期字符串 eg: Sat, 15 Oct 2022 14:02:54 +0800 + # 场景2: 中间带T的日期字符串 eg: 2020-10-14T07:48:04 + # 场景3: 中间带T的日期字符串 eg: 2020-10-14T07:48:04.208 + # 场景4: 日期字符串以GMT结尾 eg: Fri, 14 Oct 2022 07:48:04 GMT + # 场景5: 日期字符串以UTC结尾 eg: Fri, 14 Oct 2022 07:48:04 UTC + # 场景6: 日期字符串以Z结尾 eg: Fri, 14 Oct 2022 07:48:04Z + # 场景7: 日期字符串为相对时间 eg: 1 month, 2 days ago + :param datetime_str: + :return: + """ + # 传入的参数如果是None 或者空字符串 直接返回 + if not datetime_str: + return datetime_str + + try: + return dateparser.parse(datetime_str).strftime('%Y-%m-%d %H:%M:%S') + except Exception as e: + ExceptionUtils.exception_traceback(e) + return datetime_str + + @staticmethod + def timestamp_to_date(timestamp, date_format='%Y-%m-%d %H:%M:%S'): + """ + 时间戳转日期 + :param timestamp: + :param date_format: + :return: + """ + try: + return datetime.datetime.fromtimestamp(timestamp).strftime(date_format) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return timestamp + + @staticmethod + def to_bool(text, default_val: bool = False) -> bool: + """ + 字符串转bool + :param text: 要转换的值 + :param default_val: 默认值 + :return: + """ + if isinstance(text, str) and not text: + return default_val + if isinstance(text, bool): + return text + if isinstance(text, int) or isinstance(text, float): + return True if text > 0 else False + if isinstance(text, str) and text.lower() in ['y', 'true', '1']: + return True + return False + + @staticmethod + def str_from_cookiejar(cj): + """ + 将cookiejar转换为字符串 + :param cj: + :return: + """ + return '; '.join(['='.join(item) for item in cj.items()]) + + @staticmethod + def get_idlist_from_string(content, dicts): + """ + 从字符串中提取id列表 + :param content: 字符串 + :param dicts: 字典 + :return: + """ + if not content: + return [] + id_list = [] + content_list = content.split() + for dic in dicts: + if dic.get('name') in content_list and dic.get('id') not in id_list: + id_list.append(dic.get('id')) + content = content.replace(dic.get('name'), '') + return id_list, re.sub(r'\s+', ' ', content).strip() + + @staticmethod + def str_title(s): + """ + 讲英文的首字母大写 + :param s: en_name string + :return: string title + """ + return s.title() if s else s + + @staticmethod + def md5_hash(data): + """ + MD5 HASH + """ + if not data: + return "" + return hashlib.md5(str(data).encode()).hexdigest() + + @staticmethod + def str_timehours(minutes): + """ + 将分钟转换成小时和分钟 + :param minutes: + :return: + """ + if not minutes: + return "" + hours = minutes // 60 + minutes = minutes % 60 + return "%s小时%s分" % (hours, minutes) + + @staticmethod + def str_amount(amount, curr="$"): + """ + 格式化显示金额 + """ + if not amount: + return "0" + return curr + format(amount, ",") diff --git a/app/utils/system_utils.py b/app/utils/system_utils.py new file mode 100644 index 0000000..ed7da0a --- /dev/null +++ b/app/utils/system_utils.py @@ -0,0 +1,324 @@ +import datetime +import os +import platform +import shutil +import subprocess + +from app.utils.path_utils import PathUtils +from app.utils.exception_utils import ExceptionUtils +from app.utils.types import OsType +from config import WEBDRIVER_PATH + + +class SystemUtils: + + @staticmethod + def __get_hidden_shell(): + if os.name == "nt": + st = subprocess.STARTUPINFO() + st.dwFlags = subprocess.STARTF_USESHOWWINDOW + st.wShowWindow = subprocess.SW_HIDE + return st + else: + return None + + @staticmethod + def get_used_of_partition(path): + """ + 获取系统存储空间占用信息 + """ + if not path: + return 0, 0 + if not os.path.exists(path): + return 0, 0 + try: + total_b, used_b, free_b = shutil.disk_usage(path) + return used_b, total_b + except Exception as e: + ExceptionUtils.exception_traceback(e) + return 0, 0 + + @staticmethod + def get_system(): + """ + 获取操作系统类型 + """ + if SystemUtils.is_windows(): + return OsType.WINDOWS + elif SystemUtils.is_synology(): + return OsType.SYNOLOGY + elif SystemUtils.is_docker(): + return OsType.DOCKER + elif SystemUtils.is_macos(): + return OsType.MACOS + else: + return OsType.LINUX + + @staticmethod + def get_free_space_gb(folder): + """ + 计算目录剩余空间大小 + """ + total_b, used_b, free_b = shutil.disk_usage(folder) + return free_b / 1024 / 1024 / 1024 + + @staticmethod + def get_local_time(utc_time_str): + """ + 通过UTC的时间字符串获取时间 + """ + try: + utc_date = datetime.datetime.strptime(utc_time_str.replace('0000', ''), '%Y-%m-%dT%H:%M:%S.%fZ') + local_date = utc_date + datetime.timedelta(hours=8) + local_date_str = datetime.datetime.strftime(local_date, '%Y-%m-%d %H:%M:%S') + except Exception as e: + ExceptionUtils.exception_traceback(e) + return utc_time_str + return local_date_str + + @staticmethod + def check_process(pname): + """ + 检查进程序是否存在 + """ + if not pname: + return False + text = subprocess.Popen('ps -ef | grep -v grep | grep %s' % pname, shell=True).communicate() + return True if text else False + + @staticmethod + def execute(cmd): + """ + 执行命令,获得返回结果 + """ + try: + with os.popen(cmd) as p: + return p.readline().strip() + except Exception as err: + print(str(err)) + return "" + + @staticmethod + def is_docker(): + return os.path.exists('/.dockerenv') + + @staticmethod + def is_synology(): + if SystemUtils.is_windows(): + return False + return True if "synology" in SystemUtils.execute('uname -a') else False + + @staticmethod + def is_windows(): + return True if os.name == "nt" else False + + @staticmethod + def is_macos(): + return True if platform.system() == 'Darwin' else False + + @staticmethod + def is_lite_version(): + return True if SystemUtils.is_docker() \ + and os.environ.get("NASTOOL_VERSION") == "lite" else False + + @staticmethod + def get_webdriver_path(): + if SystemUtils.is_lite_version(): + return None + else: + return WEBDRIVER_PATH.get(SystemUtils.get_system().value) + + @staticmethod + def copy(src, dest): + """ + 复制 + """ + try: + shutil.copy2(os.path.normpath(src), os.path.normpath(dest)) + return 0, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def move(src, dest): + """ + 移动 + """ + try: + tmp_file = os.path.normpath(os.path.join(os.path.dirname(src), + os.path.basename(dest))) + shutil.move(os.path.normpath(src), tmp_file) + shutil.move(tmp_file, os.path.normpath(dest)) + return 0, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def link(src, dest): + """ + 硬链接 + """ + try: + if platform.release().find("-z4-") >= 0: + # 兼容极空间Z4 + tmp = os.path.normpath(os.path.join(PathUtils.get_parent_paths(dest, 2), + os.path.basename(dest))) + os.link(os.path.normpath(src), tmp) + shutil.move(tmp, os.path.normpath(dest)) + else: + os.link(os.path.normpath(src), os.path.normpath(dest)) + return 0, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def softlink(src, dest): + """ + 软链接 + """ + try: + os.symlink(os.path.normpath(src), os.path.normpath(dest)) + return 0, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def rclone_move(src, dest): + """ + Rclone移动 + """ + try: + src = os.path.normpath(src) + dest = dest.replace("\\", "/") + retcode = subprocess.run(['rclone', 'moveto', + src, + f'NASTOOL:{dest}'], + startupinfo=SystemUtils.__get_hidden_shell()).returncode + return retcode, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def rclone_copy(src, dest): + """ + Rclone复制 + """ + try: + src = os.path.normpath(src) + dest = dest.replace("\\", "/") + retcode = subprocess.run(['rclone', 'copyto', + src, + f'NASTOOL:{dest}'], + startupinfo=SystemUtils.__get_hidden_shell()).returncode + return retcode, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def minio_move(src, dest): + """ + Minio移动 + """ + try: + src = os.path.normpath(src) + dest = dest.replace("\\", "/") + if dest.startswith("/"): + dest = dest[1:] + retcode = subprocess.run(['mc', 'mv', + '--recursive', + src, + f'NASTOOL/{dest}'], + startupinfo=SystemUtils.__get_hidden_shell()).returncode + return retcode, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def minio_copy(src, dest): + """ + Minio复制 + """ + try: + src = os.path.normpath(src) + dest = dest.replace("\\", "/") + if dest.startswith("/"): + dest = dest[1:] + retcode = subprocess.run(['mc', 'cp', + '--recursive', + src, + f'NASTOOL/{dest}'], + startupinfo=SystemUtils.__get_hidden_shell()).returncode + return retcode, "" + except Exception as err: + ExceptionUtils.exception_traceback(err) + return -1, str(err) + + @staticmethod + def get_windows_drives(): + """ + 获取Windows所有盘符 + """ + vols = [] + for i in range(65, 91): + vol = chr(i) + ':' + if os.path.isdir(vol): + vols.append(vol) + return vols + + def find_hardlinks(self, file, fdir=None): + """ + 查找文件的所有硬链接 + """ + ret_files = [] + if os.name == "nt": + ret = subprocess.run( + ['fsutil', 'hardlink', 'list', file], + startupinfo=self.__get_hidden_shell(), + stdout=subprocess.PIPE + ) + if ret.returncode != 0: + return [] + if ret.stdout: + drive = os.path.splitdrive(file)[0] + link_files = ret.stdout.decode('GBK').replace('\\', '/').split('\r\n') + for link_file in link_files: + if link_file \ + and "$RECYCLE.BIN" not in link_file \ + and os.path.normpath(file) != os.path.normpath(f'{drive}{link_file}'): + link_file = f'{drive.upper()}{link_file}' + file_name = os.path.basename(link_file) + file_path = os.path.dirname(link_file) + ret_files.append({ + "file": link_file, + "filename": file_name, + "filepath": file_path + }) + else: + inode = os.stat(file).st_ino + if not fdir: + fdir = os.path.dirname(file) + stdout = subprocess.run( + ['find', fdir, '-inum', str(inode)], + stdout=subprocess.PIPE + ).stdout + if stdout: + link_files = stdout.decode('utf-8').split('\n') + for link_file in link_files: + if link_file \ + and os.path.normpath(file) != os.path.normpath(link_file): + file_name = os.path.basename(link_file) + file_path = os.path.dirname(link_file) + ret_files.append({ + "file": link_file, + "filename": file_name, + "filepath": file_path + }) + + return ret_files diff --git a/app/utils/tokens.py b/app/utils/tokens.py new file mode 100644 index 0000000..e454a2d --- /dev/null +++ b/app/utils/tokens.py @@ -0,0 +1,40 @@ +import re + +from config import SPLIT_CHARS + + +class Tokens: + _text = "" + _index = 0 + _tokens = [] + + def __init__(self, text): + self._text = text + self._tokens = [] + self.load_text(text) + + def load_text(self, text): + splited_text = re.split(r'%s' % SPLIT_CHARS, text) + for sub_text in splited_text: + if sub_text: + self._tokens.append(sub_text) + + def cur(self): + if self._index >= len(self._tokens): + return None + else: + token = self._tokens[self._index] + return token + + def get_next(self): + token = self.cur() + if token: + self._index = self._index + 1 + return token + + def peek(self): + index = self._index + 1 + if index >= len(self._tokens): + return None + else: + return self._tokens[index] diff --git a/app/utils/torrent.py b/app/utils/torrent.py new file mode 100644 index 0000000..a128886 --- /dev/null +++ b/app/utils/torrent.py @@ -0,0 +1,259 @@ +import os.path +import re +import datetime +from urllib.parse import quote, unquote + +from bencode import bdecode + +from app.utils.http_utils import RequestUtils +from config import Config + +# Trackers列表 +trackers = [ + "udp://tracker.opentrackr.org:1337/announce", + "udp://9.rarbg.com:2810/announce", + "udp://opentracker.i2p.rocks:6969/announce", + "https://opentracker.i2p.rocks:443/announce", + "udp://tracker.torrent.eu.org:451/announce", + "udp://tracker1.bt.moack.co.kr:80/announce", + "udp://tracker.pomf.se:80/announce", + "udp://tracker.moeking.me:6969/announce", + "udp://tracker.dler.org:6969/announce", + "udp://p4p.arenabg.com:1337/announce", + "udp://open.stealth.si:80/announce", + "udp://movies.zsw.ca:6969/announce", + "udp://ipv4.tracker.harry.lu:80/announce", + "udp://explodie.org:6969/announce", + "udp://exodus.desync.com:6969/announce", + "https://tracker.nanoha.org:443/announce", + "https://tracker.lilithraws.org:443/announce", + "https://tr.burnabyhighstar.com:443/announce", + "http://tracker.mywaifu.best:6969/announce", + "http://bt.okmp3.ru:2710/announce" +] + + +class Torrent: + _torrent_temp_path = None + + def __init__(self): + self._torrent_temp_path = Config().get_temp_path() + if not os.path.exists(self._torrent_temp_path): + os.makedirs(self._torrent_temp_path) + + def get_torrent_info(self, url, cookie=None, ua=None, referer=None, proxy=False): + """ + 把种子下载到本地,返回种子内容 + :param url: 种子链接 + :param cookie: 站点Cookie + :param ua: 站点UserAgent + :param referer: 关联地址,有的网站需要这个否则无法下载 + :param proxy: 是否使用内置代理 + :return: 种子保存路径、种子内容、种子文件列表主目录、种子文件列表、错误信息 + """ + if not url: + return None, None, "", [], "URL为空" + if url.startswith("magnet:"): + return None, url, "", [], f"{url} 为磁力链接" + try: + # 下载保存种子文件 + file_path, content, errmsg = self.save_torrent_file(url=url, + cookie=cookie, + ua=ua, + referer=referer, + proxy=proxy) + if not file_path: + return None, content, "", [], errmsg + # 解析种子文件 + files_folder, files, retmsg = self.get_torrent_files(file_path) + # 种子文件路径、种子内容、种子文件列表主目录、种子文件列表、错误信息 + return file_path, content, files_folder, files, retmsg + + except Exception as err: + return None, None, "", [], "下载种子文件出现异常:%s" % str(err) + + def save_torrent_file(self, url, cookie=None, ua=None, referer=None, proxy=False): + """ + 把种子下载到本地 + :return: 种子保存路径,错误信息 + """ + req = RequestUtils( + headers=ua, + cookies=cookie, + referer=referer, + proxies=Config().get_proxies() if proxy else None + ).get_res(url=url, allow_redirects=False) + while req and req.status_code in [301, 302]: + url = req.headers['Location'] + if url and url.startswith("magnet:"): + return None, url, f"获取到磁力链接:{url}" + req = RequestUtils( + headers=ua, + cookies=cookie, + referer=referer, + proxies=Config().get_proxies() if proxy else None + ).get_res(url=url, allow_redirects=False) + if req and req.status_code == 200: + if not req.content: + return None, None, "未下载到种子数据" + # 解析内容格式 + if req.text and str(req.text).startswith("magnet:"): + return None, req.text, "磁力链接" + else: + try: + bdecode(req.content) + except Exception as err: + print(str(err)) + return None, None, "种子数据有误,请确认链接是否正确,如为PT站点则需手工在站点下载一次种子" + # 读取种子文件名 + file_name = self.__get_url_torrent_filename(req, url) + # 种子文件路径 + file_path = os.path.join(self._torrent_temp_path, file_name) + # 种子内容 + file_content = req.content + # 写入磁盘 + with open(file_path, 'wb') as f: + f.write(file_content) + elif req is None: + return None, None, "无法打开链接:%s" % url + else: + return None, None, "下载种子出错,状态码:%s" % req.status_code + + return file_path, file_content, "" + + @staticmethod + def convert_hash_to_magnet(hash_text, title): + """ + 根据hash值,转换为磁力链,自动添加tracker + :param hash_text: 种子Hash值 + :param title: 种子标题 + """ + if not hash_text or not title: + return None + hash_text = re.search(r'[0-9a-z]+', hash_text, re.IGNORECASE) + if not hash_text: + return None + hash_text = hash_text.group(0) + ret_magnet = f'magnet:?xt=urn:btih:{hash_text}&dn={quote(title)}' + for tracker in trackers: + ret_magnet = f'{ret_magnet}&tr={quote(tracker)}' + return ret_magnet + + @staticmethod + def add_trackers_to_magnet(url, title=None): + """ + 添加tracker和标题到磁力链接 + """ + if not url or not title: + return None + ret_magnet = url + if title and url.find("&dn=") == -1: + ret_magnet = f'{ret_magnet}&dn={quote(title)}' + for tracker in trackers: + ret_magnet = f'{ret_magnet}&tr={quote(tracker)}' + return ret_magnet + + @staticmethod + def get_torrent_files(path): + """ + 解析Torrent文件,获取文件清单 + :return: 种子文件列表主目录、种子文件列表、错误信息 + """ + if not path or not os.path.exists(path): + return "", [], f"种子文件不存在:{path}" + file_names = [] + file_folder = "" + try: + torrent = bdecode(open(path, 'rb').read()) + if torrent.get("info"): + files = torrent.get("info", {}).get("files") or [] + if files: + for item in files: + if item.get("path"): + file_names.append(item["path"][0]) + file_folder = torrent.get("info", {}).get("name") + else: + file_names.append(torrent.get("info", {}).get("name")) + except Exception as err: + return file_folder, file_names, "解析种子文件异常:%s" % str(err) + return file_folder, file_names, "" + + def read_torrent_content(self, path): + """ + 读取本地种子文件的内容 + :return: 种子内容、种子文件列表主目录、种子文件列表、错误信息 + """ + if not path or not os.path.exists(path): + return None, "", [], "种子文件不存在:%s" % path + content, retmsg, file_folder, files = None, "", "", [] + try: + # 读取种子文件内容 + with open(path, 'rb') as f: + content = f.read() + # 解析种子文件 + file_folder, files, retmsg = self.get_torrent_files(path) + except Exception as e: + retmsg = "读取种子文件出错:%s" % str(e) + return content, file_folder, files, retmsg + + @staticmethod + def __get_url_torrent_filename(req, url): + """ + 从下载请求中获取种子文件名 + """ + if not req: + return "" + disposition = req.headers.get('content-disposition') or "" + file_name = re.findall(r"filename=\"?(.+)\"?", disposition) + if file_name: + file_name = unquote(str(file_name[0].encode('ISO-8859-1').decode()).split(";")[0].strip()) + if file_name.endswith('"'): + file_name = file_name[:-1] + elif url and url.endswith(".torrent"): + file_name = unquote(url.split("/")[-1]) + else: + file_name = str(datetime.datetime.now()) + return file_name + + @staticmethod + def get_magnet_title(url): + """ + 从磁力链接中获取标题 + """ + if not url: + return "" + title = re.findall(r"dn=(.+)&?", url) + return unquote(title[0]) if title else "" + + @staticmethod + def get_intersection_episodes(target, source, title): + """ + 对两个季集字典进行判重,有相同项目的取集的交集 + """ + if not source or not title: + return target + if not source.get(title): + return target + if not target.get(title): + target[title] = source.get(title) + return target + index = -1 + for target_info in target.get(title): + index += 1 + source_info = None + for info in source.get(title): + if info.get("season") == target_info.get("season"): + source_info = info + break + if not source_info: + continue + if not source_info.get("episodes"): + continue + if not target_info.get("episodes"): + target_episodes = source_info.get("episodes") + target[title][index]["episodes"] = target_episodes + continue + target_episodes = list(set(target_info.get("episodes")).intersection(set(source_info.get("episodes")))) + target[title][index]["episodes"] = target_episodes + return target + diff --git a/app/utils/types.py b/app/utils/types.py new file mode 100644 index 0000000..1f7956a --- /dev/null +++ b/app/utils/types.py @@ -0,0 +1,96 @@ +from enum import Enum + + +class MediaType(Enum): + TV = '电视剧' + MOVIE = '电影' + ANIME = '动漫' + UNKNOWN = '未知' + + +class DownloaderType(Enum): + QB = 'Qbittorrent' + TR = 'Transmission' + Client115 = '115网盘' + PikPak = 'PikPak' + + +class SyncType(Enum): + MAN = "手动整理" + MON = "目录同步" + + +class SearchType(Enum): + WX = "微信" + WEB = "WEB" + DB = "豆瓣" + RSS = "电影/电视剧订阅" + USERRSS = "自定义订阅" + OT = "手动下载" + TG = "Telegram" + API = "第三方API请求" + SLACK = "Slack" + SYNOLOGY = "Synology Chat" + + +class RmtMode(Enum): + LINK = "硬链接" + SOFTLINK = "软链接" + COPY = "复制" + MOVE = "移动" + RCLONECOPY = "Rclone复制" + RCLONE = "Rclone移动" + MINIOCOPY = "Minio复制" + MINIO = "Minio移动" + + +class MatchMode(Enum): + NORMAL = "正常模式" + STRICT = "严格模式" + + +class OsType(Enum): + WINDOWS = "Windows" + LINUX = "Linux" + SYNOLOGY = "Synology" + MACOS = "MacOS" + DOCKER = "Docker" + + +class IndexerType(Enum): + BUILTIN = "Indexer" + + +class MediaServerType(Enum): + JELLYFIN = "Jellyfin" + EMBY = "Emby" + PLEX = "Plex" + + +class BrushDeleteType(Enum): + NOTDELETE = "不删除" + SEEDTIME = "做种时间" + RATIO = "分享率" + UPLOADSIZE = "上传量" + DLTIME = "下载耗时" + AVGUPSPEED = "平均上传速度" + IATIME = "未活动时间" + + +# 站点框架 +class SiteSchema(Enum): + DiscuzX = "Discuz!" + Gazelle = "Gazelle" + Ipt = "IPTorrents" + NexusPhp = "NexusPhp" + NexusProject = "NexusProject" + NexusRabbit = "NexusRabbit" + SmallHorse = "Small Horse" + Unit3d = "Unit3d" + TorrentLeech = "TorrentLeech" + FileList = "FileList" + TNode = "TNode" + + +MovieTypes = ['MOV', '电影'] +TvTypes = ['TV', '电视剧'] diff --git a/build_sites.py b/build_sites.py new file mode 100644 index 0000000..68521fd --- /dev/null +++ b/build_sites.py @@ -0,0 +1,17 @@ +import os.path +import pickle +import ruamel.yaml +from app.utils.path_utils import PathUtils +from config import Config + + +if __name__ == "__main__": + _indexers = [] + _site_path = os.path.join(Config().get_config_path(), "sites") + cfg_files = PathUtils.get_dir_files(in_path=_site_path, exts=[".yml"]) + for cfg_file in cfg_files: + with open(cfg_file, mode='r', encoding='utf-8') as f: + print(cfg_file) + _indexers.append(ruamel.yaml.YAML().load(f)) + with open(os.path.join(Config().get_inner_config_path(), "sites.dat"), 'wb') as f: + pickle.dump(_indexers, f, pickle.HIGHEST_PROTOCOL) diff --git a/check_config.py b/check_config.py new file mode 100644 index 0000000..678475e --- /dev/null +++ b/check_config.py @@ -0,0 +1,753 @@ +import json +import os +from werkzeug.security import generate_password_hash +from app.helper import DbHelper +from app.utils import StringUtils, ExceptionUtils +from config import Config + + +def check_config(): + """ + 检查配置文件,如有错误进行日志输出 + """ + # 检查日志输出 + if Config().get_config('app'): + logtype = Config().get_config('app').get('logtype') + if logtype: + print("日志输出类型为:%s" % logtype) + if logtype == "server": + logserver = Config().get_config('app').get('logserver') + if not logserver: + print("【Config】日志中心地址未配置,无法正常输出日志") + else: + print("日志将上送到服务器:%s" % logserver) + elif logtype == "file": + logpath = Config().get_config('app').get('logpath') + if not logpath: + print("【Config】日志文件路径未配置,无法正常输出日志") + else: + print("日志将写入文件:%s" % logpath) + + # 检查WEB端口 + web_port = Config().get_config('app').get('web_port') + if not web_port: + print("WEB服务端口未设置,将使用默认3000端口") + + # 检查登录用户和密码 + login_user = Config().get_config('app').get('login_user') + login_password = Config().get_config('app').get('login_password') + if not login_user or not login_password: + print("WEB管理用户或密码未设置,将使用默认用户:admin,密码:password") + else: + print("WEB管理页面用户:%s" % str(login_user)) + + # 检查HTTPS + ssl_cert = Config().get_config('app').get('ssl_cert') + ssl_key = Config().get_config('app').get('ssl_key') + if not ssl_cert or not ssl_key: + print("未启用https,请使用 http://IP:%s 访问管理页面" % str(web_port)) + else: + if not os.path.exists(ssl_cert): + print("ssl_cert文件不存在:%s" % ssl_cert) + if not os.path.exists(ssl_key): + print("ssl_key文件不存在:%s" % ssl_key) + print("已启用https,请使用 https://IP:%s 访问管理页面" % str(web_port)) + + rmt_tmdbkey = Config().get_config('app').get('rmt_tmdbkey') + if not rmt_tmdbkey: + print("TMDB API Key未配置,媒体整理、搜索下载等功能将无法正常运行!") + rmt_match_mode = Config().get_config('app').get('rmt_match_mode') + if rmt_match_mode: + rmt_match_mode = rmt_match_mode.upper() + else: + rmt_match_mode = "NORMAL" + if rmt_match_mode == "STRICT": + print("TMDB匹配模式:严格模式") + else: + print("TMDB匹配模式:正常模式") + else: + print("配置文件格式错误,找不到app配置项!") + + # 检查媒体库目录路径 + if Config().get_config('media'): + media_server = Config().get_config('media').get('media_server') + if media_server: + print("媒体管理软件设置为:%s" % media_server) + if media_server == "jellyfin": + if not Config().get_config('jellyfin'): + print("jellyfin未配置") + else: + if not Config().get_config('jellyfin').get('host') \ + or not Config().get_config('jellyfin').get('api_key'): + print("jellyfin配置不完整") + elif media_server == "plex": + if not Config().get_config('plex'): + print("plex未配置") + else: + if not Config().get_config('plex').get('token') \ + and not Config().get_config('plex').get('username'): + print("plex配置不完整") + else: + if not Config().get_config('emby'): + print("emby未配置") + else: + if not Config().get_config('emby').get('host') \ + or not Config().get_config('emby').get('api_key'): + print("emby配置不完整") + + movie_paths = Config().get_config('media').get('movie_path') + if not movie_paths: + print("未配置电影媒体库目录") + else: + if not isinstance(movie_paths, list): + movie_paths = [movie_paths] + for movie_path in movie_paths: + if not os.path.exists(movie_path): + print("电影媒体库目录不存在:%s" % movie_path) + + tv_paths = Config().get_config('media').get('tv_path') + if not tv_paths: + print("未配置电视剧媒体库目录") + else: + if not isinstance(tv_paths, list): + tv_paths = [tv_paths] + for tv_path in tv_paths: + if not os.path.exists(tv_path): + print("电视剧媒体库目录不存在:%s" % tv_path) + + anime_paths = Config().get_config('media').get('anime_path') + if anime_paths: + if not isinstance(anime_paths, list): + anime_paths = [anime_paths] + for anime_path in anime_paths: + if not os.path.exists(anime_path): + print("动漫媒体库目录不存在:%s" % anime_path) + + category = Config().get_config('media').get('category') + if not category: + print("未配置分类策略") + else: + print("配置文件格式错误,找不到media配置项!") + + # 检查站点配置 + if Config().get_config('pt'): + pt_client = Config().get_config('pt').get('pt_client') + print("下载软件设置为:%s" % pt_client) + + rmt_mode = Config().get_config('pt').get('rmt_mode', 'copy') + if rmt_mode == "link": + print("默认文件转移模式为:硬链接") + elif rmt_mode == "softlink": + print("默认文件转移模式为:软链接") + elif rmt_mode == "move": + print("默认文件转移模式为:移动") + elif rmt_mode == "rclone": + print("默认文件转移模式为:rclone移动") + elif rmt_mode == "rclonecopy": + print("默认文件转移模式为:rclone复制") + else: + print("默认文件转移模式为:复制") + + search_indexer = Config().get_config('pt').get('search_indexer') + if search_indexer: + print("索引器设置为:%s" % search_indexer) + + search_auto = Config().get_config('pt').get('search_auto') + if search_auto: + print("微信等移动端渠道搜索已开启自动择优下载") + + ptsignin_cron = Config().get_config('pt').get('ptsignin_cron') + if not ptsignin_cron: + print("站点自动签到时间未配置,站点签到功能已关闭") + + pt_check_interval = Config().get_config('pt').get('pt_check_interval') + if not pt_check_interval: + print("RSS订阅周期未配置,RSS订阅功能已关闭") + + pt_monitor = Config().get_config('pt').get('pt_monitor') + if not pt_monitor: + print("下载软件监控未开启,下载器监控功能已关闭") + else: + print("配置文件格式错误,找不到pt配置项!") + + # 检查Douban配置 + if not Config().get_config('douban'): + print("豆瓣未配置") + else: + if not Config().get_config('douban').get('users') \ + or not Config().get_config('douban').get('types') \ + or not Config().get_config('douban').get('days'): + print("豆瓣配置不完整") + + +def update_config(): + """ + 升级配置文件 + """ + _config = Config().get_config() + _dbhelper = DbHelper() + overwrite_cofig = False + + # 密码初始化 + login_password = _config.get("app", {}).get("login_password") or "password" + if login_password and not login_password.startswith("[hash]"): + _config['app']['login_password'] = "[hash]%s" % generate_password_hash( + login_password) + overwrite_cofig = True + + # 实验室配置初始化 + if not _config.get("laboratory"): + _config['laboratory'] = { + 'search_keyword': False, + 'tmdb_cache_expire': True, + 'use_douban_titles': False, + 'search_en_title': True, + 'chrome_browser': False + } + overwrite_cofig = True + + # 安全配置初始化 + if not _config.get("security"): + _config['security'] = { + 'media_server_webhook_allow_ip': { + 'ipv4': '0.0.0.0/0', + 'ipv6': '::/0' + }, + 'telegram_webhook_allow_ip': { + 'ipv4': '127.0.0.1', + 'ipv6': '::/0' + } + } + overwrite_cofig = True + + # Synology Chat安全配置初始化 + if not _config.get("security", {}).get("synology_webhook_allow_ip"): + _config['security']['synology_webhook_allow_ip'] = { + 'ipv4': '127.0.0.1', + 'ipv6': '::/0' + } + overwrite_cofig = True + + # API密钥初始化 + if not _config.get("security", {}).get("api_key"): + _config['security']['api_key'] = _config.get("security", + {}).get("subscribe_token") \ + or StringUtils.generate_random_str() + if _config.get('security', {}).get('subscribe_token'): + _config['security'].pop('subscribe_token') + overwrite_cofig = True + + # 刮削NFO配置初始化 + if not _config.get("scraper_nfo"): + _config['scraper_nfo'] = { + "movie": { + "basic": True, + "credits": True, + "credits_chinese": False}, + "tv": { + "basic": True, + "credits": True, + "credits_chinese": False, + "season_basic": True, + "episode_basic": True, + "episode_credits": True} + } + overwrite_cofig = True + + # 刮削图片配置初始化 + if not _config.get("scraper_pic"): + _config['scraper_pic'] = { + "movie": { + "poster": True, + "backdrop": True, + "background": True, + "logo": True, + "disc": True, + "banner": True, + "thumb": True}, + "tv": { + "poster": True, + "backdrop": True, + "background": True, + "logo": True, + "clearart": True, + "banner": True, + "thumb": True, + "season_poster": True, + "season_banner": True, + "season_thumb": True, + "episode_thumb": False} + } + overwrite_cofig = True + + # 下载目录配置初始化 + if not _config.get('downloaddir'): + dl_client = _config.get('pt', {}).get('pt_client') + if dl_client and _config.get(dl_client): + save_path = _config.get(dl_client).get('save_path') + if not isinstance(save_path, dict): + save_path = {"movie": save_path, + "tv": save_path, "anime": save_path} + container_path = _config.get(dl_client).get('save_containerpath') + if not isinstance(container_path, dict): + container_path = {"movie": container_path, + "tv": container_path, "anime": container_path} + downloaddir = [] + type_dict = {"movie": "电影", "tv": "电视剧", "anime": "动漫"} + for mtype, path in save_path.items(): + if not path: + continue + save_dir = path.split('|')[0] + save_label = None + if len(path.split('|')) > 1: + save_label = path.split('|')[1] + container_dir = container_path.get(mtype) + if save_dir: + downloaddir.append({"save_path": save_dir, + "type": type_dict.get(mtype), + "category": "", + "container_path": container_dir, + "label": save_label}) + _config['downloaddir'] = downloaddir + if _config.get('qbittorrent', {}).get('save_path'): + _config['qbittorrent'].pop('save_path') + if _config.get('qbittorrent', {}).get('save_containerpath'): + _config['qbittorrent'].pop('save_containerpath') + if _config.get('transmission', {}).get('save_path'): + _config['transmission'].pop('save_path') + if _config.get('transmission', {}).get('save_containerpath'): + _config['transmission'].pop('save_containerpath') + if _config.get('client115', {}).get('save_path'): + _config['client115'].pop('save_path') + if _config.get('client115', {}).get('save_containerpath'): + _config['client115'].pop('save_containerpath') + if _config.get('pikpak', {}).get('save_path'): + _config['pikpak'].pop('save_path') + if _config.get('pikpak', {}).get('save_containerpath'): + _config['pikpak'].pop('save_containerpath') + overwrite_cofig = True + elif isinstance(_config.get('downloaddir'), dict): + downloaddir_list = [] + for path, attr in _config.get('downloaddir').items(): + downloaddir_list.append({"save_path": path, + "type": attr.get("type"), + "category": attr.get("category"), + "container_path": attr.get("path"), + "label": attr.get("label")}) + _config['downloaddir'] = downloaddir_list + overwrite_cofig = True + + # 自定义识别词兼容旧配置 + try: + ignored_words = Config().get_config('laboratory').get("ignored_words") + if ignored_words: + ignored_words = ignored_words.split("||") + for ignored_word in ignored_words: + if not _dbhelper.is_custom_words_existed(replaced=ignored_word): + _dbhelper.insert_custom_word(replaced=ignored_word, + replace="", + front="", + back="", + offset=0, + wtype=1, + gid=-1, + season=-2, + enabled=1, + regex=1, + whelp="") + _config['laboratory'].pop('ignored_words') + overwrite_cofig = True + replaced_words = Config().get_config('laboratory').get("replaced_words") + if replaced_words: + replaced_words = replaced_words.split("||") + for replaced_word in replaced_words: + replaced_word = replaced_word.split("@") + if not _dbhelper.is_custom_words_existed(replaced=replaced_word[0]): + _dbhelper.insert_custom_word(replaced=replaced_word[0], + replace=replaced_word[1], + front="", + back="", + offset=0, + wtype=2, + gid=-1, + season=-2, + enabled=1, + regex=1, + whelp="") + _config['laboratory'].pop('replaced_words') + overwrite_cofig = True + offset_words = Config().get_config('laboratory').get("offset_words") + if offset_words: + offset_words = offset_words.split("||") + for offset_word in offset_words: + offset_word = offset_word.split("@") + if not _dbhelper.is_custom_words_existed(front=offset_word[0], back=offset_word[1]): + _dbhelper.insert_custom_word(replaced="", + replace="", + front=offset_word[0], + back=offset_word[1], + offset=offset_word[2], + wtype=4, + gid=-1, + season=-2, + enabled=1, + regex=1, + whelp="") + _config['laboratory'].pop('offset_words') + overwrite_cofig = True + except Exception as e: + ExceptionUtils.exception_traceback(e) + + # 目录同步兼容旧配置 + try: + sync_paths = Config().get_config('sync').get('sync_path') + rmt_mode = Config().get_config('pt').get('sync_mod') + if sync_paths: + if isinstance(sync_paths, list): + for sync_items in sync_paths: + SyncPath = {'from': "", + 'to': "", + 'unknown': "", + 'syncmod': rmt_mode, + 'rename': 1, + 'enabled': 1} + # 是否启用 + if sync_items.startswith("#"): + SyncPath['enabled'] = 0 + sync_items = sync_items[1:-1] + # 是否重命名 + if sync_items.startswith("["): + SyncPath['rename'] = 0 + sync_items = sync_items[1:-1] + # 转移方式 + config_items = sync_items.split("@") + if not config_items: + continue + if len(config_items) > 1: + SyncPath['syncmod'] = config_items[-1] + else: + SyncPath['syncmod'] = rmt_mode + if not SyncPath['syncmod']: + continue + # 源目录|目的目录|未知目录 + paths = config_items[0].split("|") + if not paths: + continue + if len(paths) > 0: + if not paths[0]: + continue + SyncPath['from'] = os.path.normpath(paths[0]) + if len(paths) > 1: + SyncPath['to'] = os.path.normpath(paths[1]) + if len(paths) > 2: + SyncPath['unknown'] = os.path.normpath(paths[2]) + # 相同from的同步目录不能同时开启 + if SyncPath['enabled'] == 1: + _dbhelper.check_config_sync_paths(source=SyncPath['from'], + enabled=0) + _dbhelper.insert_config_sync_path(source=SyncPath['from'], + dest=SyncPath['to'], + unknown=SyncPath['unknown'], + mode=SyncPath['syncmod'], + rename=SyncPath['rename'], + enabled=SyncPath['enabled']) + else: + _dbhelper.insert_config_sync_path(source=sync_paths, + dest="", + unknown="", + mode=rmt_mode, + rename=1, + enabled=0) + _config['sync'].pop('sync_path') + overwrite_cofig = True + except Exception as e: + ExceptionUtils.exception_traceback(e) + + # 消息服务兼容旧配置 + try: + message = Config().get_config('message') or {} + msg_channel = message.get('msg_channel') + if msg_channel: + switchs = [] + switch = message.get('switch') + if switch: + if switch.get("download_start"): + switchs.append("download_start") + if switch.get("download_fail"): + switchs.append("download_fail") + if switch.get("transfer_finished"): + switchs.append("transfer_finished") + if switch.get("transfer_fail"): + switchs.append("transfer_fail") + if switch.get("rss_added"): + switchs.append("rss_added") + if switch.get("rss_finished"): + switchs.append("rss_finished") + if switch.get("site_signin"): + switchs.append("site_signin") + switchs.append('site_message') + switchs.append('brushtask_added') + switchs.append('brushtask_remove') + switchs.append('mediaserver_message') + if message.get('telegram'): + token = message.get('telegram', {}).get('telegram_token') + chat_id = message.get('telegram', {}).get('telegram_chat_id') + user_ids = message.get('telegram', {}).get('telegram_user_ids') + webhook = message.get('telegram', {}).get('webhook') + if token and chat_id: + name = "Telegram" + ctype = 'telegram' + enabled = 1 if msg_channel == ctype else 0 + interactive = 1 if enabled else 0 + client_config = json.dumps({ + 'token': token, + 'chat_id': chat_id, + 'user_ids': user_ids, + 'webhook': webhook + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + if message.get('wechat'): + corpid = message.get('wechat', {}).get('corpid') + corpsecret = message.get('wechat', {}).get('corpsecret') + agent_id = message.get('wechat', {}).get('agentid') + default_proxy = message.get('wechat', {}).get('default_proxy') + token = message.get('wechat', {}).get('Token') + encodingAESkey = message.get( + 'wechat', {}).get('EncodingAESKey') + if corpid and corpsecret and agent_id: + name = "WeChat" + ctype = 'wechat' + enabled = 1 if msg_channel == ctype else 0 + interactive = 1 if enabled else 0 + client_config = json.dumps({ + 'corpid': corpid, + 'corpsecret': corpsecret, + 'agentid': agent_id, + 'default_proxy': default_proxy, + 'token': token, + 'encodingAESKey': encodingAESkey + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + if message.get('serverchan'): + sckey = message.get('serverchan', {}).get('sckey') + if sckey: + name = "ServerChan" + ctype = 'serverchan' + interactive = 0 + enabled = 1 if msg_channel == ctype else 0 + client_config = json.dumps({ + 'sckey': sckey + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + if message.get('bark'): + server = message.get('bark', {}).get('server') + apikey = message.get('bark', {}).get('apikey') + if server and apikey: + name = "Bark" + ctype = 'bark' + interactive = 0 + enabled = 1 if msg_channel == ctype else 0 + client_config = json.dumps({ + 'server': server, + 'apikey': apikey + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + if message.get('pushplus'): + token = message.get('pushplus', {}).get('push_token') + topic = message.get('pushplus', {}).get('push_topic') + channel = message.get('pushplus', {}).get('push_channel') + webhook = message.get('pushplus', {}).get('push_webhook') + if token and channel: + name = "PushPlus" + ctype = 'pushplus' + interactive = 0 + enabled = 1 if msg_channel == ctype else 0 + client_config = json.dumps({ + 'token': token, + 'topic': topic, + 'channel': channel, + 'webhook': webhook + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + if message.get('iyuu'): + token = message.get('iyuu', {}).get('iyuu_token') + if token: + name = "IyuuMsg" + ctype = 'iyuu' + interactive = 0 + enabled = 1 if msg_channel == ctype else 0 + client_config = json.dumps({ + 'token': token + }) + _dbhelper.insert_message_client(name=name, + ctype=ctype, + config=client_config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + # 删除旧配置 + if _config.get('message', {}).get('msg_channel'): + _config['message'].pop('msg_channel') + if _config.get('message', {}).get('switch'): + _config['message'].pop('switch') + if _config.get('message', {}).get('wechat'): + _config['message'].pop('wechat') + if _config.get('message', {}).get('telegram'): + _config['message'].pop('telegram') + if _config.get('message', {}).get('serverchan'): + _config['message'].pop('serverchan') + if _config.get('message', {}).get('bark'): + _config['message'].pop('bark') + if _config.get('message', {}).get('pushplus'): + _config['message'].pop('pushplus') + if _config.get('message', {}).get('iyuu'): + _config['message'].pop('iyuu') + overwrite_cofig = True + except Exception as e: + ExceptionUtils.exception_traceback(e) + + # 站点兼容旧配置 + try: + sites = _dbhelper.get_config_site() + for site in sites: + if not site.NOTE or str(site.NOTE).find('{') != -1: + continue + # 是否解析种子详情为|分隔的第1位 + site_parse = str(site.NOTE).split("|")[0] or "Y" + # 站点过滤规则为|分隔的第2位 + rule_groupid = str(site.NOTE).split("|")[1] if site.NOTE and len( + str(site.NOTE).split("|")) > 1 else "" + # 站点未读消息为|分隔的第3位 + site_unread_msg_notify = str(site.NOTE).split("|")[2] if site.NOTE and len( + str(site.NOTE).split("|")) > 2 else "Y" + # 自定义UA为|分隔的第4位 + ua = str(site.NOTE).split("|")[3] if site.NOTE and len( + str(site.NOTE).split("|")) > 3 else "" + # 是否开启浏览器仿真为|分隔的第5位 + chrome = str(site.NOTE).split("|")[4] if site.NOTE and len( + str(site.NOTE).split("|")) > 4 else "N" + # 是否使用代理为|分隔的第6位 + proxy = str(site.NOTE).split("|")[5] if site.NOTE and len( + str(site.NOTE).split("|")) > 5 else "N" + _dbhelper.update_config_site_note(tid=site.ID, note=json.dumps({ + "parse": site_parse, + "rule": rule_groupid, + "message": site_unread_msg_notify, + "ua": ua, + "chrome": chrome, + "proxy": proxy + })) + + except Exception as e: + ExceptionUtils.exception_traceback(e) + + # 订阅兼容旧配置 + try: + def __parse_rss_desc(desc): + rss_sites = [] + search_sites = [] + over_edition = False + restype = None + pix = None + team = None + rule = None + total = None + current = None + notes = str(desc).split('#') + # 订阅站点 + if len(notes) > 0: + if notes[0]: + rss_sites = [s for s in str(notes[0]).split( + '|') if s and len(s) < 20] + # 搜索站点 + if len(notes) > 1: + if notes[1]: + search_sites = [s for s in str(notes[1]).split('|') if s] + # 洗版 + if len(notes) > 2: + over_edition = notes[2] + # 过滤条件 + if len(notes) > 3: + if notes[3]: + filters = notes[3].split('@') + if len(filters) > 0: + restype = filters[0] + if len(filters) > 1: + pix = filters[1] + if len(filters) > 2: + rule = int( + filters[2]) if filters[2].isdigit() else None + if len(filters) > 3: + team = filters[3] + # 总集数及当前集数 + if len(notes) > 4: + if notes[4]: + ep_info = notes[4].split('@') + if len(ep_info) > 0: + total = int(ep_info[0]) if ep_info[0] else None + if len(ep_info) > 1: + current = int(ep_info[1]) if ep_info[1] else None + return { + "rss_sites": rss_sites, + "search_sites": search_sites, + "over_edition": over_edition, + "restype": restype, + "pix": pix, + "team": team, + "rule": rule, + "total": total, + "current": current + } + + # 电影订阅 + rss_movies = _dbhelper.get_rss_movies() + for movie in rss_movies: + if not movie.DESC or str(movie.DESC).find('#') == -1: + continue + # 更新到具体字段 + _dbhelper.update_rss_movie_desc( + rid=movie.ID, + desc=json.dumps(__parse_rss_desc(movie.DESC)) + ) + # 电视剧订阅 + rss_tvs = _dbhelper.get_rss_tvs() + for tv in rss_tvs: + if not tv.DESC or str(tv.DESC).find('#') == -1: + continue + # 更新到具体字段 + _dbhelper.update_rss_tv_desc( + rid=tv.ID, + desc=json.dumps(__parse_rss_desc(tv.DESC)) + ) + + except Exception as e: + ExceptionUtils.exception_traceback(e) + + # 重写配置文件 + if overwrite_cofig: + Config().save_config(_config) diff --git a/config.py b/config.py new file mode 100644 index 0000000..9d3a7f2 --- /dev/null +++ b/config.py @@ -0,0 +1,193 @@ +import os +import shutil +import sys +from threading import Lock +import ruamel.yaml + +# 种子名/文件名要素分隔字符 +SPLIT_CHARS = r"\.|\s+|\(|\)|\[|]|-|\+|【|】|/|~|;|&|\||#|_|「|」|(|)|~" +# 默认User-Agent +DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36" +# 收藏了的媒体的目录名,名字可以改,在Emby中点击红星则会自动将电影转移到此分类下,需要在Emby Webhook中配置用户行为通知 +RMT_FAVTYPE = '精选' +# 支持的媒体文件后缀格式 +RMT_MEDIAEXT = ['.mp4', '.mkv', '.ts', '.iso', + '.rmvb', '.avi', '.mov', '.mpeg', + '.mpg', '.wmv', '.3gp', '.asf', + '.m4v', '.flv', '.m2ts', '.strm'] +# 支持的字幕文件后缀格式 +RMT_SUBEXT = ['.srt', '.ass', '.ssa'] +# 电视剧动漫的分类genre_ids +ANIME_GENREIDS = ['16'] +# 默认过滤的文件大小,150M +RMT_MIN_FILESIZE = 150 * 1024 * 1024 +# 删种检查时间间隔 +AUTO_REMOVE_TORRENTS_INTERVAL = 1800 +# 下载文件转移检查时间间隔, +PT_TRANSFER_INTERVAL = 300 +# TMDB信息缓存定时保存时间 +METAINFO_SAVE_INTERVAL = 600 +# SYNC目录同步聚合转移时间 +SYNC_TRANSFER_INTERVAL = 60 +# RSS队列中处理时间间隔 +RSS_CHECK_INTERVAL = 300 +# 站点流量数据刷新时间间隔(小时) +REFRESH_PT_DATA_INTERVAL = 6 +# 刷新订阅TMDB数据的时间间隔(小时) +RSS_REFRESH_TMDB_INTERVAL = 6 +# 刷流删除的检查时间间隔 +BRUSH_REMOVE_TORRENTS_INTERVAL = 300 +# 定时清除未识别的缓存时间间隔(小时) +META_DELETE_UNKNOWN_INTERVAL = 12 +# 定时刷新壁纸的间隔(小时) +REFRESH_WALLPAPER_INTERVAL = 1 +# fanart的api,用于拉取封面图片 +FANART_MOVIE_API_URL = 'https://webservice.fanart.tv/v3/movies/%s?api_key=d2d31f9ecabea050fc7d68aa3146015f' +FANART_TV_API_URL = 'https://webservice.fanart.tv/v3/tv/%s?api_key=d2d31f9ecabea050fc7d68aa3146015f' +# 默认背景图地址 +DEFAULT_TMDB_IMAGE = 'https://s3.bmp.ovh/imgs/2022/07/10/77ef9500c851935b.webp' +# 默认微信消息代理服务器地址 +DEFAULT_WECHAT_PROXY = 'https://wechat.nastool.cn' +# 默认OCR识别服务地址 +DEFAULT_OCR_SERVER = 'https://nastool.cn' +# 默认TMDB代理服务地址 +DEFAULT_TMDB_PROXY = 'https://tmdb.nastool.cn' +# 默认CookieCloud服务地址 +DEFAULT_COOKIECLOUD_SERVER = 'http://nastool.cn:8088' +# TMDB图片地址 +TMDB_IMAGE_W500_URL = 'https://image.tmdb.org/t/p/w500%s' +TMDB_IMAGE_ORIGINAL_URL = 'https://image.tmdb.org/t/p/original%s' +TMDB_IMAGE_FACE_URL = 'https://image.tmdb.org/t/p/h632%s' +TMDB_PEOPLE_PROFILE_URL = 'https://www.themoviedb.org/person/%s' +# 添加下载时增加的标签,开始只监控NASTool添加的下载时有效 +PT_TAG = "NASTOOL" +# 电影默认命名格式 +DEFAULT_MOVIE_FORMAT = '{title} ({year})/{title} ({year})-{part} - {videoFormat}' +# 电视剧默认命名格式 +DEFAULT_TV_FORMAT = '{title} ({year})/Season {season}/{title} - {season_episode}-{part} - 第 {episode} 集' +# 辅助识别参数 +KEYWORD_SEARCH_WEIGHT_1 = [10, 3, 2, 0.5, 0.5] +KEYWORD_SEARCH_WEIGHT_2 = [10, 2, 1] +KEYWORD_SEARCH_WEIGHT_3 = [10, 2] +KEYWORD_STR_SIMILARITY_THRESHOLD = 0.2 +KEYWORD_DIFF_SCORE_THRESHOLD = 30 +KEYWORD_BLACKLIST = ['中字', '韩语', '双字', '中英', '日语', '双语', '国粤', 'HD', 'BD', '中日', '粤语', '完全版', + '法语', '西班牙语', 'HRHDTVAC3264', '未删减版', '未删减', '国语', '字幕组', '人人影视', 'www66ystv', + '人人影视制作', '英语', 'www6vhaotv', '无删减版', '完成版', '德意'] + +# WebDriver路径 +WEBDRIVER_PATH = { + "Docker": "/usr/lib/chromium/chromedriver", + "Synology": "/var/packages/NASTool/target/bin/chromedriver" +} + +# Xvfb虚拟显示路程 +XVFB_PATH = [ + "/usr/bin/Xvfb", + "/usr/local/bin/Xvfb" +] + +# 线程锁 +lock = Lock() + +# 全局实例 +_CONFIG = None + + +def singleconfig(cls): + def _singleconfig(*args, **kwargs): + global _CONFIG + if not _CONFIG: + with lock: + _CONFIG = cls(*args, **kwargs) + return _CONFIG + + return _singleconfig + + +@singleconfig +class Config(object): + _config = {} + _config_path = None + + def __init__(self): + self._config_path = os.environ.get('NASTOOL_CONFIG') + if not os.environ.get('TZ'): + os.environ['TZ'] = 'Asia/Shanghai' + self.init_syspath() + self.init_config() + + def init_config(self): + try: + if not self._config_path: + print("【Config】NASTOOL_CONFIG 环境变量未设置,程序无法工作,正在退出...") + quit() + if not os.path.exists(self._config_path): + cfg_tp_path = os.path.join(self.get_inner_config_path(), "config.yaml") + cfg_tp_path = cfg_tp_path.replace("\\", "/") + shutil.copy(cfg_tp_path, self._config_path) + print("【Config】config.yaml 配置文件不存在,已将配置文件模板复制到配置目录...") + with open(self._config_path, mode='r', encoding='utf-8') as cf: + try: + # 读取配置 + print("正在加载配置:%s" % self._config_path) + self._config = ruamel.yaml.YAML().load(cf) + except Exception as e: + print("【Config】配置文件 config.yaml 格式出现严重错误!请检查:%s" % str(e)) + self._config = {} + except Exception as err: + print("【Config】加载 config.yaml 配置出错:%s" % str(err)) + return False + + def init_syspath(self): + with open(os.path.join(self.get_root_path(), + "third_party.txt"), "r") as f: + for third_party_lib in f.readlines(): + module_path = os.path.join(self.get_root_path(), + "third_party", + third_party_lib.strip()).replace("\\", "/") + if module_path not in sys.path: + sys.path.append(module_path) + + def get_proxies(self): + return self.get_config('app').get("proxies") + + def get_ua(self): + return self.get_config('app').get("user_agent") or DEFAULT_UA + + def get_config(self, node=None): + if not node: + return self._config + return self._config.get(node, {}) + + def save_config(self, new_cfg): + self._config = new_cfg + with open(self._config_path, mode='w', encoding='utf-8') as sf: + yaml = ruamel.yaml.YAML() + return yaml.dump(new_cfg, sf) + + def get_config_path(self): + return os.path.dirname(self._config_path) + + def get_temp_path(self): + return os.path.join(self.get_config_path(), "temp") + + @staticmethod + def get_root_path(): + return os.path.dirname(os.path.realpath(__file__)) + + def get_inner_config_path(self): + return os.path.join(self.get_root_path(), "config") + + def get_script_path(self): + return os.path.join(self.get_inner_config_path(), "scripts") + + def get_domain(self): + domain = (self.get_config('app') or {}).get('domain') + if domain and not domain.startswith('http'): + domain = "http://" + domain + return domain + + @staticmethod + def get_timezone(): + return os.environ.get('TZ') diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..0624add --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,320 @@ +# 【配置注意要符合yaml语法,:号后有1个空格,不能使用全角标点符号】 +# 【最新版本已经可以通过WEB页面对所有配置项进行配置,推荐使用WEB页面进行配置】 +# 【文件转移方式的说明】 +# 目前支持的文件转移方式:link、copy、softlink、move、rclone、rclonecopy,link即硬链接、softlink为软链接、copy为复制、move为移动、rclone针对rclone网盘挂载(rclone为移动、rclonecopy为复制) +# link要求源目录和目的目录或媒体库目录在一个磁盘分区或者存储空间,Docker运行时link模式需要直接映射源目录和目的目录或媒体库目录的上级目录,否则docker可能仍然会认为是跨盘 +# softlink模式注意宿主机的源目录映射到docker容器中后要路径要一致,否则可能软链接成功但无法在宿主机使用 +# copy模式会直接复制一份文件数据 +# move会直接移动原文件,会影响做种,请谨慎使用 +# rclone需要自行映射rclone配置目录到容器中,或在容器内完成rclone配置 +app: + # 【日志记录类型】:server、file、console + # 如果是使用Docker安装建议设置为console,通过Docker管理器查看日志 + # 如果是使用群晖套件建议配置为 server,可将日志输出到群晖的日志中心便于查看 + # 其它情况可以设置为file,将日志写入文件 + logtype: console + # 【日志文件的路径】:logtype为file时生效 + logpath: + # 【群晖日志中心IP和端口】:logtype为SERVER时生效。端口一般是514,只需要改动IP为群晖的IP,示例:127.0.0.1:514 + logserver: 127.0.0.1:514 + # 【日志级别】:info、debug、error + loglevel: info + # 【WEB管理界面监听地址】:如需支持ipv6需设置为::,如::无法访问可改为0.0.0.0 + web_host: "::" + # 【WEB管理界面端口】:默认3000 + web_port: 3000 + # 【WEB管理页面登录用户】,默认admin + login_user: admin + # 【WEB管理页面登录密码】:默认password,如果是全数字密码,要用''括起来 + login_password: password + # 【WEB管理界面使用的HTTPS的证书和KEY的路径】,留空则不启用HTTPS + ssl_cert: + ssl_key: + # 【TMDB API KEY】:需要在https://www.themoviedb.org/申请,必须配置,否则无法识别媒体资源和重命名 + # 以下地址需要网络能够正常访问:api.themoviedb.org、webservice.fanart.tv + rmt_tmdbkey: + # 【使用TMDB服务器域名】:api.themoviedb.org、api.tmdb.org,如api.themoviedb.org无法访问可偿试使用api.tmdb.org + tmdb_domain: api.tmdb.org + # 【TMDB匹配模式】:normal、strict,normal模式下如使用文件名/种子名中的年份无法匹配到媒体信息,会去掉年份再匹配一次;strict模式则严格按文件中年份匹配 + # normal模式下会提升识别成功率,但也可能会导致误识别率增加;strict模式可以降低误识别率,但可能导致很多文件名/种子名中年份不正确的无法被识别(特别是剧集,需要是首播年份) + rmt_match_mode: normal + # 【设置代理】,themoviedb、fanart、telegram等将使用代理访问,http和https均需配置,可以是http也可以是socks5、socks5h(remote DNS) ,但需要带http或socks5前缀,两项可以配置为一样,留空则不启用 + # 示例:'http://127.0.0.1:7890' 'socks5://127.0.0.1:8018' 'socks5h://127.0.0.1:8018' + proxies: + http: + https: + # 【本系统的WEB的外网地址】:需要是外网IP或者域名,需要包含端口,用于微信/Telegram信息点击跳转,如不需要可配空 + # 示例:http://IP:3000 + domain: "" + # 【UserAgent】:可适当修改,用于站点签到、豆瓣数据抓取等 + user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36" + # 【登录界面壁纸】:themoviedb、bing,设置为themoviedb时需要配置TMDB API Key时才生效 + wallpaper: bing + # Debug mode + debug: true + +# 【配置媒体库信息】 +media: + # 【媒体库管理软件】:emby、jellyfin、plex,需要在emby或jellyfin或plex区配置详细信息,用于下载检查控重、媒体库展示等,建议配置 + media_server: emby + # 【媒体库数据同步周期】:定时同步媒体服务器数据到本地,单位小时 + mediasync_interval: 12 + # 【媒体库电影文件存放目录】:支持配置多个目录,不同的硬盘需映射为不同的根目录,以更于程序区分 + movie_path: + # 【媒体库电视剧文件存放目录】:支持配置多个目录,不同的硬盘需映射为不同的根目录,以更于程序区分 + tv_path: + # 【媒体库动漫文件单独存放目录】:支持配置多个目录,不同的硬盘需映射为不同的根目录,以更于程序区分 + # 如果设置了该目录,则所有动漫电视剧都会识别为动漫并存放在该目录下,否则动漫电视剧会识别为电视剧并存放在电视剧目录分类下;动漫电影仍然在电影目录分类下 + anime_path: + # 【无法识别时转移存放的目录】:如有多个磁盘,需要对应配置多个目录,否则跨盘无法硬链接 + # 注意:如果你在sync区域配置了未识别目录,由会优先转移到对应未识别目录下,只有下载文件转移及sync未配置未识别目录时才会使用该目录 + # 未识别的记录同时会在媒体整理->手动识别下面出现,unknown_path只是硬链接一份用于备份,同时手工识别处理后程序也不会主动删除,如果不想要多硬链接一份,可以不配置该目录 + unknown_path: + # 【二级分类开关】:电影/电视剧/动漫是否需要二级分类,启用二级分类后会在电影/电视剧/动漫目录下按二级分类名建立子目录 + # 此处配置分类的策略名,配置文件目录中需要有与策略名同名的.yaml配置文件 + # 默认策略default-category分类设置可参考"default-category.yaml",分类参见README.MD说明 + # 如不需要启动分类,则该项配置为空 + category: "default-category" + # 【转移到媒体库的最小文件大小】:避免预告片/MV等影响识别,单位M + min_filesize: 150 + # 【文件名转移忽略词】:文件名包含忽略词,忽略转移 + ignored_files: + # 【文件路径转移忽略词】:文件路径包含忽略词,忽略转移 + ignored_paths: + # 【洗版开关】:如开启则则新下载了更大的文件会覆盖媒体库目录中已有的文件 + filesize_cover: true + # 【电影命名定义】:程序会按定义的命名格式对电影进行重命名;/代表上下级目录,{}内为占位符;占位符会使用文件识别出来的实际值替换;占位符外的字符会当成普通字符,直接体现在名称上 + # 电影占位符有:{title}:标题,{en_title}:英文标题,{original_title}:原语种标题,{original_name}:原文件名,{year}:年份,{edition}:版本(Bluray/WEB-DL等),{videoFormat}:分辨率(1080p/4k等),{videoCodec}:视频编码,{audioCodec}:音频编码及声道,{effect}: 视频特效(DV,HDR等), {tmdbid}:TMDB的ID,{part}:part1/disc1/dvd1,{releaseGroup}:制作组/字幕组等 + movie_name_format: "{title} ({year})/{title}-{part} ({year}) - {videoFormat}" + # 【电视剧命名定义】:程序会按定义的命名格式对电视剧进行重命名;/代表上下级目录,{}内为占位符;占位符会使用文件识别出来的实际值替换,占位符外的字符会当成普通字符,直接体现在名称上 + # 电视剧占位符有:{title}:标题,{en_title}:英文标题,{original_title}:原语种标题,{original_name}:原文件名,{year}:年份,{edition}:版本(Bluray/WEB-DL等),{videoFormat}:分辨率(1080p/4k等),{videoCodec}:视频编码,{audioCodec}:音频编码及声道,{effect}: 视频特效(DV,HDR等), {tmdbid}:TMDB的ID,{season}:季数,{episode}:集数,{season_episode}:剧集SxxExx,{part}:part1/disc1/dvd1,{releaseGroup}:制作组/字幕组等 + tv_name_format: "{title} ({year})/Season {season}/{title}-{part} - {season_episode} - 第{episode}集" + # 【刮削元数据及图片】:开启后文件转移完成时会自动生成nfo描述文件及poster海报,协助媒体服务器识别和搜刮 + nfo_poster: false + # 【实时刷新媒体库】:开启后文件转移完成时会实时刷新媒体服务器(Emby/Jellyfin/Plex)的媒体库 + refresh_mediaserver: true + +# 配置Emby服务器信息 +emby: + # 【Emby服务器IP地址和端口】:注意区分http和https,http时可以不加http://,https时必须加https:// + host: http://127.0.0.1:8096 + # 【Emby ApiKey】:在Emby设置->高级->API密钥处生成,注意不要复制到了应用名称 + api_key: + +# 配置Jellyfin服务器信息 +jellyfin: + # 【Jellyfin服务器IP地址和端口】:注意区分http和https,http时可以不加http://,https时必须加https:// + host: http://127.0.0.1:8096 + # 【Jellyfin ApiKey】:在Jellyfin设置->高级->API密钥处生成 + api_key: + +# 配置Plex服务器信息 +plex: + # 【Plex服务器IP地址和端口】:注意区分http和https,http时可以不加http://,https时必须加https:// + host: http://127.0.0.1:32400 + # 【X-Plex-Token】:Plex页面Cookie中的X-Plex-Token,如填写token则无需填写servername、username、password + token: + # 【Plex服务器的名称】 + servername: + # 【Plex用户名】 + username: + # 【Plex用户密码】 + password: + +# 【配置nfo刮削信息】 +scraper_nfo: + # 电影 + movie: + basic: true + credits: true + credits_chinese: true + # 电视剧 + tv: + basic: true + credits: true + credits_chinese: true + # 季 + season_basic: true + # 集 + episode_basic: true + episode_credits: true + +# 【配置图片刮削信息】 +scraper_pic: + # 电影 + movie: + poster: true + backdrop: true + background: true + logo: true + disc: true + banner: true + thumb: true + # 电视剧 + tv: + poster: true + backdrop: true + background: true + logo: true + clearart: true + banner: true + thumb: true + # 季 + season_poster: true + season_banner: true + season_thumb: true + # 集 + episode_thumb: false + +# 【配置消息通知服务】 +message: + # 【Emby播放状态通知白名单】:配置了Emby webhooks插件回调时,用户播放媒体库中的媒体时会发送消息通知,本处配置哪些用户的设备不通知,避免打扰,配置格式:用户:设备名称,可用 - 增加多项 + webhook_ignore: + +# 【配置文件夹监控】:文件夹内容发生变化时自动识别转移 +sync: + # 监控目录配置已转移至数据库 + # 【监控目录操作系统类型】:windows、linux。如果是windows,目录同步功能性能会比较差,会导致NAS不能休眠,除非是挂载的windows的远程共享目录或者是windows的docker,否则建议设置为linux + nas_sys: linux + +# 【配置站点检索信息】 +pt: + # 【下载使用的客户端软件】:qbittorrent、transmission、client115等 + pt_client: qbittorrent + # 【下载软件监控开关】:是否监控下载软件:true、false,如为true则下载完成会自动转移和重命名,如为false则不会处理 + # 下载软件监控与Sync下载目录同步不要同时开启,否则功能存在重复 + pt_monitor: false + # 【只监控NASTool添加的下载】:启用后只有NASTool添加的下载才会被自动转移和显示,关闭则下载软件中所有的任务都会转移和显示 + pt_monitor_only: true + # 【下载完成后转移到媒体库的转移模式】:link、copy、softlink、move、rclone、rclonecopy、minio、miniocopy,详情参考顶部说明 + rmt_mode: link + #【聚合检索使用的检索器】:builtin + search_indexer: builtin + # 【内建索引器使用的站点】:只有在该站点列表中内建索引器搜索时才会使用 + indexer_sites: + # 【远程搜索自动择优下载开关】:如开启则微信等渠道搜索后会自动择优选择一项下载,如不开启则需要手工点击进入WEB页面选择下载 + # 如没有配置app.domain或无公网环境建议开启,否则无法跳转WEB页面手工选择 + search_auto: true + # 【远程下载不完整自动订阅】:如开启,远程搜索下载不完整时,会自动添加RSS订阅 + search_no_result_rss: false + # 【站点每日签到时间】 + # 两种配置方法,1、配置间隔,单位小时,建议不要设置为24小时的整数倍,避免每天的签到时间一样。2、配置固定时间,如'08:00',注意要加引号和冒号。3、配置时间范围,如08:00-09:00,表示在该时间范围内随机执行一次 + ptsignin_cron: "08:01" + # 【RSS订阅开关】:此处配置RSS订阅检查时间间隔,即每隔多长时间检查一下各站点是否有资源更新,建议不要少于30分钟,单位时间为秒 + # 配置为空或者0则不启用RSS订阅功能 + pt_check_interval: 1800 + # 【定量搜索RSS开关】:打开后,每隔设置时间会通过站点资源检索的方式查询和下载订阅,单位:小时,配置小于6小时时强制为6小时,不配置则为关 + search_rss_interval: 6 + # 【下载优先规则】:订阅及远程搜索下载将按此优先规则选择下载资源,字典:site 站点优先、seeder做种数优先 + download_order: site + # 【搜索结果数量限制】:每个站点返回搜索结果的最大数量 + site_search_result_num: 100 + +# 【配置qBittorrent下载软件】:pt区的pt_client如配置为qbittorrent则需要同步配置该项 +qbittorrent: + # 【qBittorrent IP地址和端口】:注意如果qb启动了HTTPS证书,则需要配置为https://IP + qbhost: + qbport: + # qBittorrent 登录用户名和密码 + qbusername: + qbpassword: + # 转移完成后是否自动强制作种,按需要设置 + force_upload: true + # 是否开始自动管理模式 + auto_management: false + +# 【配置transmission下载软件】:pt区的pt_client如配置为transmission则需要同步配置该项,需要3.0以上版本,否则可能会报错 +transmission: + # 【transmission IP地址和端口】:注意如果tr启用了HTTPS证书,则需要配置为https://IP + trhost: + trport: + # transmission 登录用户名和密码 + trusername: + trpassword: + +# 配置 115 网盘下载器 +client115: + # 115 Cookie 抓包获取 + cookie: + +# 配置 pikpak 网盘下载器 +pikpak: + # 用户名 + username: + # 密码 + password: + # 代理 + proxy: + +# 【下载目录】:配置下载目录,自按分类下载到指定目录 +downloaddir: + +# 【配置豆瓣账号信息】:配置后会自动同步豆瓣收藏,豆瓣标记想看内容后,后台自动下载 +douban: + # 【用户ID列表】:豆瓣电影点个我主页people后面的那一串数字,或者使用豆瓣App个人信息中查看。可以配置多个,注意要加引号 + # 这里可以是自己的,也可以是别人的,比如填写几个大V的账号ID,实现热门影视自动下载 + users: + - "" + # 【豆瓣Cookie】:选配,嫌麻烦的可以不用配置,可能影响个别电影的同步 + cookie: + # 【同步天数】:同步多少天内加入的数据 + days: 30 + # 【同步间隔】:多久同步一次数据,单位小时,建议不要太频繁,避免被检测到后封号 + interval: + # 【同步数据类型】:同步哪些类型的收藏数据:do 在看,wish 想看,collect 看过,用逗号分隔配置 + types: "wish" + # 【自动开载开关】:同步到豆瓣的数据后是否自动检索站点并下载 + auto_search: true + # 【自动添加RSS开关】:站点检索找不到的记录是否自动添加RSS订阅(可实现未搜索到的自动追更) + auto_rss: true + +# 【配置字幕自动下载】 +subtitle: + # 【下载渠道】:opensubtitles、chinesesubfinder + server: opensubtitles + # opensubtitles.org + opensubtitles: + # 是否启用 + enable: true + # 配置ChineseSubFinder的服务器地址和API Key,API Key在ChineseSubFinder->配置中心->实验室->API Key处生成 + chinesesubfinder: + # IP地址和端口 + host: + # API KEY + api_key: + # NASTOOL媒体的映射路径 + local_path: + # ChineseSubFinder媒体的映射路径 + remote_path: + +# 【配置安全】 +security: + # 【媒体服务器webhook允许ip范围】:即只有如下范围的IP才允许调用webhook + media_server_webhook_allow_ip: + ipv4: 0.0.0.0/0 + ipv6: ::/0 + # 【Telegram webhook允许ip范围】:即只有如下范围的IP才允许调用webhook + telegram_webhook_allow_ip: + ipv4: 127.0.0.1 + ipv6: ::/0 + # 【Synology Chat webhook允许ip范围】:即只有如下范围的IP才允许调用webhook + synology_webhook_allow_ip: + ipv4: 127.0.0.1 + ipv6: ::/0 + # 【API认证密钥】:用于Jellyseerr、Overseerr中Authorization认证以及非客户端类的API调用 + api_key: + +# 【实验室】 +laboratory: + # 【识别增强】关键字猜想 + search_keyword: false + # 【识别增强】通过TMDB WEB检索 + search_tmdbweb: false + # 【TMDB缓存过期策略】:是否开启TMDB缓存过期策略,默认7天过期,过期缓存将被删除, 7天内访问过期时间可以被刷新 + tmdb_cache_expire: true + # 【使用豆瓣名称联想】:开启将使用豆瓣进行电影电视剧的名称联想,否则使用TMDB的数据 + use_douban_titles: false + # 【精确搜索使用英文名称】:开启后对于精确搜索场景(远程搜索、订阅搜索等)将会使用英文名检索站点资源以提升匹配度,但对有些站点资源标题全是中文的则需要关闭,否则匹配不到 + search_en_title: true + # 【使用TMDB代理】 + tmdb_proxy: false diff --git a/config/default-category.yaml b/config/default-category.yaml new file mode 100644 index 0000000..c5d857d --- /dev/null +++ b/config/default-category.yaml @@ -0,0 +1,219 @@ +# 配置电影的分类策略, 配置为空或者不配置该项则不启用电影分类 +movie: + # 分类名同时也是目录名,会按先后顺序匹配,匹配后程序会按这个名称建立二级目录 + 华语电影: + # 分类依据,可以是:original_language 语种、production_countries(电影)/origin_country(电视剧) 国家或地区、genre_ids 内容类型等,只要TMDB API返回的字段中有就行 + # 配置多项条件时,需要同时满足;不需要的匹配项可以删掉或者配置为空 + # 匹配值对应用,号分隔,这里是匹配语种 + original_language: 'zh,cn,bo,za' + 动画电影: + # 匹配 genre_ids 内容类型,16是动漫 + genre_ids: '16' + # 未配置任何过滤条件时,则按先后顺序不符合上面分类的都会在这个分类下,建议配置在最末尾 + 外语电影: + +# 配置电视剧的分类策略, 配置为空或者不配置该项则不启用电视剧分类 +tv: + # 分类名同时也是目录名,会按先后顺序匹配,匹配后程序会按这个名称建立二级目录 + # 如果有配置动漫独立目录,则实际上不会使用到tv下的动漫二级分类 + 动漫: + # 匹配 genre_ids 内容类型,16是动漫 + genre_ids: '16' + 纪录片: + # 匹配 genre_ids 内容类型,99是纪录片 + genre_ids: '99' + 儿童: + # 匹配 genre_ids 内容类型,10762是儿童 + genre_ids: '10762' + 综艺: + # 匹配 genre_ids 内容类型,10764 10767都是综艺 + genre_ids: '10764,10767' + 国产剧: + # 匹配 origin_country 国家,CN是中国大陆,TW是中国台湾,HK是中国香港 + origin_country: 'CN,TW,HK' + 欧美剧: + # 匹配 origin_country 国家,主要欧美国家列表 + origin_country: 'US,FR,GB,DE,ES,IT,NL,PT,RU,UK' + 日韩剧: + # 匹配 origin_country 国家,主要亚洲国家列表 + origin_country: 'JP,KP,KR,TH,IN,SG' + # 未匹配以上分类,则命名为未分类 + 未分类: + +# 配置动漫的分类策略, 配置为空或者不配置该项则不启用动漫分类 +anime: + # 如果你的anime_path动漫目录已经直接设置到了动漫子目录,则这个分类可以取消 + 动漫: + # 匹配 genre_ids 内容类型,16是动漫 + genre_ids: '16' + +## genre_ids 内容类型 字典,注意部分中英文是不一样的 +# 28 Action +# 12 Adventure +# 16 Animation +# 35 Comedy +# 80 Crime +# 99 Documentary +# 18 Drama +# 10751 Family +# 14 Fantasy +# 36 History +# 27 Horror +# 10402 Music +# 9648 Mystery +# 10749 Romance +# 878 Science Fiction +# 10770 TV Movie +# 53 Thriller +# 10752 War +# 37 Western +# 28 动作 +# 12 冒险 +# 16 动画 +# 35 喜剧 +# 80 犯罪 +# 99 纪录 +# 18 剧情 +# 10751 家庭 +# 14 奇幻 +# 36 历史 +# 27 恐怖 +# 10402 音乐 +# 9648 悬疑 +# 10749 爱情 +# 878 科幻 +# 10770 电视电影 +# 53 惊悚 +# 10752 战争 +# 37 西部 + +## original_language 语种 字典 +# af 南非语 +# ar 阿拉伯语 +# az 阿塞拜疆语 +# be 比利时语 +# bg 保加利亚语 +# ca 加泰隆语 +# cs 捷克语 +# cy 威尔士语 +# da 丹麦语 +# de 德语 +# dv 第维埃语 +# el 希腊语 +# en 英语 +# eo 世界语 +# es 西班牙语 +# et 爱沙尼亚语 +# eu 巴士克语 +# fa 法斯语 +# fi 芬兰语 +# fo 法罗语 +# fr 法语 +# gl 加里西亚语 +# gu 古吉拉特语 +# he 希伯来语 +# hi 印地语 +# hr 克罗地亚语 +# hu 匈牙利语 +# hy 亚美尼亚语 +# id 印度尼西亚语 +# is 冰岛语 +# it 意大利语 +# ja 日语 +# ka 格鲁吉亚语 +# kk 哈萨克语 +# kn 卡纳拉语 +# ko 朝鲜语 +# kok 孔卡尼语 +# ky 吉尔吉斯语 +# lt 立陶宛语 +# lv 拉脱维亚语 +# mi 毛利语 +# mk 马其顿语 +# mn 蒙古语 +# mr 马拉地语 +# ms 马来语 +# mt 马耳他语 +# nb 挪威语(伯克梅尔) +# nl 荷兰语 +# ns 北梭托语 +# pa 旁遮普语 +# pl 波兰语 +# pt 葡萄牙语 +# qu 克丘亚语 +# ro 罗马尼亚语 +# ru 俄语 +# sa 梵文 +# se 北萨摩斯语 +# sk 斯洛伐克语 +# sl 斯洛文尼亚语 +# sq 阿尔巴尼亚语 +# sv 瑞典语 +# sw 斯瓦希里语 +# syr 叙利亚语 +# ta 泰米尔语 +# te 泰卢固语 +# th 泰语 +# tl 塔加路语 +# tn 茨瓦纳语 +# tr 土耳其语 +# ts 宗加语 +# tt 鞑靼语 +# uk 乌克兰语 +# ur 乌都语 +# uz 乌兹别克语 +# vi 越南语 +# xh 班图语 +# zh 中文 +# cn 中文 +# zu 祖鲁语 + +## origin_country 国家地区 字典 +# AR 阿根廷 +# AU 澳大利亚 +# BE 比利时 +# BR 巴西 +# CA 加拿大 +# CH 瑞士 +# CL 智利 +# CO 哥伦比亚 +# CZ 捷克 +# DE 德国 +# DK 丹麦 +# EG 埃及 +# ES 西班牙 +# FR 法国 +# GR 希腊 +# HK 香港 +# IL 以色列 +# IN 印度 +# IQ 伊拉克 +# IR 伊朗 +# IT 意大利 +# JP 日本 +# MM 缅甸 +# MO 澳门 +# MX 墨西哥 +# MY 马来西亚 +# NL 荷兰 +# NO 挪威 +# PH 菲律宾 +# PK 巴基斯坦 +# PL 波兰 +# RU 俄罗斯 +# SE 瑞典 +# SG 新加坡 +# TH 泰国 +# TR 土耳其 +# US 美国 +# VN 越南 +# CN 中国 内地 +# GB 英国 +# TW 中国台湾 +# NZ 新西兰 +# SA 沙特阿拉伯 +# LA 老挝 +# KP 朝鲜 北朝鲜 +# KR 韩国 南朝鲜 +# PT 葡萄牙 +# MN 蒙古国 蒙古 diff --git a/config/scripts/init_filter.sql b/config/scripts/init_filter.sql new file mode 100644 index 0000000..044e6ba --- /dev/null +++ b/config/scripts/init_filter.sql @@ -0,0 +1,100 @@ +INSERT OR IGNORE INTO "CONFIG_FILTER_GROUP" ("ID","GROUP_NAME","IS_DEFAULT","NOTE") VALUES + (1000,'日常观影','N',NULL); +INSERT OR IGNORE INTO "CONFIG_FILTER_RULES" ("ID","GROUP_ID","ROLE_NAME","PRIORITY","INCLUDE","EXCLUDE","SIZE_LIMIT","NOTE") VALUES + (10000,'1000','1080p特效-bluray','1','特效 +1080[pi] +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10001,'1000','1080p中字-bluray','2','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +1080[pi] +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10002,'1000','4k特效-bluray','3','特效 +4k|2160p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10003,'1000','4k中字-bluray','4','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +4k|2160p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10004,'1000','高清特效-bluray','5','特效 +720p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10005,'1000','高清中字-bluray','6','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +720p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10006,'1000','1080p-bluray','7','1080[pi] +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10007,'1000','4k-bluray','8','4k|2160p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10008,'1000','高清-bluray','9','720p +blu-?ray +[Hx].?26[45]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10025,'1000','1080p特效-其他来源','1','特效 +1080[pi]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10026,'1000','1080p中字-其他来源','2','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +1080[pi]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10027,'1000','4k特效-其他来源','3','特效 +4k|2160p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10028,'1000','4k中字-其他来源','4','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +4k|2160p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','1,30',NULL), + (10029,'1000','高清特效-其他来源','5','特效 +720p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10030,'1000','高清中字-其他来源','6','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +720p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10031,'1000','1080p-其他来源','7','1080[pi]','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10032,'1000','4k-其他来源','8','4k|2160p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL), + (10033,'1000','高清-其他来源','9','720p','Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|\Wsdr\W|minibd|[\W_]diy[\W_]|[\W_]3d[\W_]|REMUX','30',NULL); +INSERT OR IGNORE INTO "CONFIG_FILTER_GROUP" ("ID","GROUP_NAME","IS_DEFAULT","NOTE") VALUES + (1001,'洗版收藏','N',NULL); +INSERT OR IGNORE INTO "CONFIG_FILTER_RULES" ("ID","GROUP_ID","ROLE_NAME","PRIORITY","INCLUDE","EXCLUDE","SIZE_LIMIT","NOTE") VALUES + (10009,'1001','DIY典藏-4K-原盘','1','Mbps@Audies|Oldboys +4k|2160p +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10010,'1001','DIY典藏-1080p-原盘','2','Mbps@Audies|Oldboys +1080[pi] +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10011,'1001','特效典藏-4K-原盘','3','特效 +4k|2160p +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10012,'1001','特效典藏-1080p-原盘','4','特效 +1080[pi] +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10013,'1001','中字典藏-4K-原盘','5','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +4k|2160p +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10014,'1001','中字典藏-1080p-原盘','6','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +1080[pi] +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10015,'1001','典藏-4K-原盘','7','4k|2160p +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10016,'1001','典藏-1080p-原盘','8','1080[pi] +Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC','[Hx].?26[45]','20,99',NULL), + (10017,'1001','DIY典藏-4K-REMUX','1','Mbps@Audies|Oldboys +4k|2160p +remux','[Hx].?26[45]','20,99',NULL), + (10018,'1001','DIY典藏-1080p-REMUX','2','Mbps@Audies|Oldboys +1080[pi] +remux','[Hx].?26[45]','20,99',NULL), + (10019,'1001','特效典藏-4K-REMUX','3','特效 +4k|2160p +remux','[Hx].?26[45]','20,99',NULL), + (10020,'1001','特效典藏-1080p-REMUX','4','特效 +1080[pi] +remux','[Hx].?26[45]','20,99',NULL), + (10021,'1001','中字典藏-4K-REMUX','5','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +4k|2160p +remux','[Hx].?26[45]','20,99',NULL), + (10022,'1001','中字典藏-1080p-REMUX','6','[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文 +1080[pi] +remux','[Hx].?26[45]','20,99',NULL), + (10023,'1001','典藏-4K-REMUX','7','4k|2160p +remux','[Hx].?26[45]','20,99',NULL), + (10024,'1001','典藏-1080p-REMUX','8','1080[pi] +remux','[Hx].?26[45]','20,99',NULL); +INSERT OR IGNORE INTO "CONFIG_FILTER_GROUP" ("ID","GROUP_NAME","IS_DEFAULT","NOTE") VALUES + (9999,'不过滤','Y',NULL); \ No newline at end of file diff --git a/config/scripts/init_userrss_v3.sql b/config/scripts/init_userrss_v3.sql new file mode 100644 index 0000000..1235aaf --- /dev/null +++ b/config/scripts/init_userrss_v3.sql @@ -0,0 +1,100 @@ +INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('1', '通用', 'XML', '{ + "list": "//channel/item", + "item": { + "title": { + "path": ".//title/text()" + }, + "enclosure": { + "path": ".//enclosure[@type=''application/x-bittorrent'']/@url" + }, + "link": { + "path": ".//link/text()" + }, + "date": { + "path": ".//pubDate/text()" + }, + "description": { + "path": ".//description/text()" + }, + "size": { + "path": ".//link/@length" + } + } +}', '', '', 'Y'); +INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('2', '蜜柑计划', 'XML', '{ + "list": "//channel/item", + "item": { + "title": { + "path": ".//title/text()" + }, + "enclosure": { + "path": ".//enclosure[@type=''application/x-bittorrent'']/@url" + }, + "link": { + "path": "link/text()", + "namespaces": "https://mikanani.me/0.1/" + }, + "date": { + "path": "pubDate/text()", + "namespaces": "https://mikanani.me/0.1/" + }, + "description": { + "path": ".//description/text()" + }, + "size": { + "path": ".//enclosure[@type=''application/x-bittorrent'']/@length" + } + } +}', '', '', 'Y'); +INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('3', 'TMDB电影片单', 'JSON', '{ + "list": "$.items", + "item": { + "title": { + "path": "title" + }, + "year": { + "path": "release_date" + }, + "type": { + "value": "movie" + } + } +}', 'api_key={TMDBKEY}&language=zh-CN', '', 'Y'); +INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('4', 'TMDB电视剧片单', 'JSON', '{ + "list": "$.items", + "item": { + "title": { + "path": "name" + }, + "year": { + "path": "first_air_date" + }, + "type": { + "value": "tv" + } + } +}', 'api_key={TMDBKEY}&language=zh-CN', '', 'Y'); +INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('5', 'Nyaa', 'XML', '{ + "list": "//channel/item", + "item": { + "title": { + "path": ".//title/text()" + }, + "enclosure": { + "path": ".//link/text()" + }, + "link": { + "path": ".//guid/text()" + }, + "date": { + "path": ".//pubDate/text()" + }, + "description": { + "path": ".//description/text()" + }, + "size": { + "path": "size/text()", + "namespaces": "https://nyaa.si/xmlns/nyaa" + } + } +}', '', '', 'Y'); \ No newline at end of file diff --git a/config/scripts/reset_db_version.sql b/config/scripts/reset_db_version.sql new file mode 100644 index 0000000..9083322 --- /dev/null +++ b/config/scripts/reset_db_version.sql @@ -0,0 +1 @@ +delete from alembic_version where 1 \ No newline at end of file diff --git a/config/scripts/update_subscribe.sql b/config/scripts/update_subscribe.sql new file mode 100644 index 0000000..7f97acb --- /dev/null +++ b/config/scripts/update_subscribe.sql @@ -0,0 +1,2 @@ +UPDATE RSS_MOVIES SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; +UPDATE RSS_TVS SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; diff --git a/config/scripts/update_userpris.sql b/config/scripts/update_userpris.sql new file mode 100644 index 0000000..5b1e5b7 --- /dev/null +++ b/config/scripts/update_userpris.sql @@ -0,0 +1 @@ +UPDATE main.CONFIG_USERS SET PRIS = replace(PRIS, '推荐', '探索') WHERE 1 \ No newline at end of file diff --git a/config/scripts/update_userrss.sql b/config/scripts/update_userrss.sql new file mode 100644 index 0000000..7fc540c --- /dev/null +++ b/config/scripts/update_userrss.sql @@ -0,0 +1 @@ +UPDATE CONFIG_USER_RSS SET PROCESS_COUNT = '0' WHERE PROCESS_COUNT is null \ No newline at end of file diff --git a/config/sites.dat b/config/sites.dat new file mode 100644 index 0000000..1301d43 Binary files /dev/null and b/config/sites.dat differ diff --git a/db_scripts/README b/db_scripts/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/db_scripts/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/db_scripts/env.py b/db_scripts/env.py new file mode 100644 index 0000000..0192775 --- /dev/null +++ b/db_scripts/env.py @@ -0,0 +1,80 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from app.db.models import Base +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + render_as_batch=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/db_scripts/script.py.mako b/db_scripts/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/db_scripts/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/db_scripts/versions/720a6289a697_1_1_0.py b/db_scripts/versions/720a6289a697_1_1_0.py new file mode 100644 index 0000000..6eb4d57 --- /dev/null +++ b/db_scripts/versions/720a6289a697_1_1_0.py @@ -0,0 +1,150 @@ +"""1.1.0 + +Revision ID: 720a6289a697 +Revises: None +Create Date: 2023-01-22 08:18:00.723780 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '720a6289a697' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # 1.0.0 + op.execute('DROP TABLE IF EXISTS IGNORED_WORDS') + op.execute('DROP TABLE IF EXISTS REPLACED_WORDS') + op.execute('DROP TABLE IF EXISTS OFFSET_WORDS') + try: + with op.batch_alter_table("CUSTOM_WORDS") as batch_op: + batch_op.alter_column('OFFSET', type_=sa.Text, existing_type=sa.Integer) + except Exception as e: + print(str(e)) + # 1.0.1 + try: + with op.batch_alter_table("CONFIG_USER_RSS") as batch_op: + batch_op.add_column(sa.Column('SAVE_PATH', sa.Text)) + batch_op.add_column(sa.Column('DOWNLOAD_SETTING', sa.Integer)) + except Exception as e: + print(str(e)) + # 1.0.2 + try: + with op.batch_alter_table("RSS_MOVIES") as batch_op: + batch_op.add_column(sa.Column('RSS_SITES', sa.Text)) + batch_op.add_column(sa.Column('SEARCH_SITES', sa.Text)) + batch_op.add_column(sa.Column('OVER_EDITION', sa.Integer)) + batch_op.add_column(sa.Column('FILTER_RESTYPE', sa.Text)) + batch_op.add_column(sa.Column('FILTER_PIX', sa.Text)) + batch_op.add_column(sa.Column('FILTER_RULE', sa.Integer)) + batch_op.add_column(sa.Column('FILTER_TEAM', sa.Text)) + batch_op.add_column(sa.Column('SAVE_PATH', sa.Text)) + batch_op.add_column(sa.Column('DOWNLOAD_SETTING', sa.Integer)) + batch_op.add_column(sa.Column('FUZZY_MATCH', sa.Integer)) + batch_op.add_column(sa.Column('NOTE', sa.Text)) + except Exception as e: + print(str(e)) + try: + with op.batch_alter_table("RSS_TVS") as batch_op: + batch_op.add_column(sa.Column('RSS_SITES', sa.Text)) + batch_op.add_column(sa.Column('SEARCH_SITES', sa.Text)) + batch_op.add_column(sa.Column('OVER_EDITION', sa.Integer)) + batch_op.add_column(sa.Column('FILTER_RESTYPE', sa.Text)) + batch_op.add_column(sa.Column('FILTER_PIX', sa.Text)) + batch_op.add_column(sa.Column('FILTER_RULE', sa.Integer)) + batch_op.add_column(sa.Column('FILTER_TEAM', sa.Text)) + batch_op.add_column(sa.Column('SAVE_PATH', sa.Text)) + batch_op.add_column(sa.Column('DOWNLOAD_SETTING', sa.Integer)) + batch_op.add_column(sa.Column('FUZZY_MATCH', sa.Integer)) + batch_op.add_column(sa.Column('TOTAL_EP', sa.Integer)) + batch_op.add_column(sa.Column('CURRENT_EP', sa.Integer)) + batch_op.add_column(sa.Column('NOTE', sa.Text)) + except Exception as e: + print(str(e)) + # 1.0.3 + try: + with op.batch_alter_table("TRANSFER_HISTORY") as batch_op: + batch_op.alter_column('FILE_PATH', new_column_name="SOURCE_PATH", existing_type=sa.Text) + batch_op.alter_column('FILE_NAME', new_column_name="SOURCE_FILENAME", existing_type=sa.Text) + batch_op.alter_column('SE', new_column_name="SEASON_EPISODE", existing_type=sa.Text) + batch_op.add_column(sa.Column('TMDBID', sa.Integer)) + batch_op.add_column(sa.Column('DEST_PATH', sa.Text)) + batch_op.add_column(sa.Column('DEST_FILENAME', sa.Text)) + except Exception as e: + print(str(e)) + try: + with op.batch_alter_table("DOWNLOAD_SETTING") as batch_op: + batch_op.add_column(sa.Column('DOWNLOADER', sa.Text)) + except Exception as e: + print(str(e)) + # 1.0.7 + try: + with op.batch_alter_table("TRANSFER_UNKNOWN") as batch_op: + batch_op.add_column(sa.Column('MODE', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + # 1.0.8 + try: + with op.batch_alter_table("CONFIG_USER_RSS") as batch_op: + batch_op.add_column(sa.Column('RECOGNIZATION', sa.Text, nullable=True)) + batch_op.add_column(sa.Column('MEDIAINFOS', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + # 1.0.9 + try: + with op.batch_alter_table("SITE_USER_INFO_STATS") as batch_op: + batch_op.drop_column('FAVICON') + except Exception as e: + print(e) + try: + with op.batch_alter_table("DOUBAN_MEDIAS") as batch_op: + batch_op.add_column(sa.Column('ADD_TIME', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + try: + with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: + batch_op.add_column(sa.Column('SENDMESSAGE', sa.Text, nullable=True)) + batch_op.add_column(sa.Column('FORCEUPLOAD', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + # 1.0.10 + try: + with op.batch_alter_table("RSS_MOVIES") as batch_op: + batch_op.add_column(sa.Column('FILTER_ORDER', sa.Integer, nullable=True)) + except Exception as e: + print(str(e)) + try: + with op.batch_alter_table("RSS_TVS") as batch_op: + batch_op.add_column(sa.Column('FILTER_ORDER', sa.Integer, nullable=True)) + except Exception as e: + print(str(e)) + # 1.0.11 + try: + with op.batch_alter_table("RSS_MOVIES") as batch_op: + batch_op.add_column(sa.Column('KEYWORD', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + try: + with op.batch_alter_table("RSS_TVS") as batch_op: + batch_op.add_column(sa.Column('KEYWORD', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + # 1.0.12 + try: + with op.batch_alter_table("CONFIG_USER_RSS") as batch_op: + batch_op.add_column(sa.Column('OVER_EDITION', sa.Integer, nullable=True)) + batch_op.add_column(sa.Column('SITES', sa.Text, nullable=True)) + batch_op.add_column(sa.Column('FILTER_ARGS', sa.Text, nullable=True)) + except Exception as e: + print(str(e)) + # ### end Alembic commands ### + + +def downgrade() -> None: + pass diff --git a/dbscript_gen.py b/dbscript_gen.py new file mode 100644 index 0000000..d5e4b52 --- /dev/null +++ b/dbscript_gen.py @@ -0,0 +1,12 @@ +import os +from config import Config +from alembic.config import Config as AlembicConfig +from alembic.command import revision as alembic_revision + +db_version = input("请输入版本号:") +db_location = os.path.join(Config().get_config_path(), 'user.db').replace('\\', '/') +script_location = os.path.join(os.path.dirname(__file__), 'db_scripts').replace('\\', '/') +alembic_cfg = AlembicConfig() +alembic_cfg.set_main_option('script_location', script_location) +alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") +alembic_revision(alembic_cfg, db_version, True) diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..293e513 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +FROM alpine +RUN apk add --no-cache libffi-dev \ + && apk add --no-cache $(echo $(wget --no-check-certificate -qO- https://raw.githubusercontent.com/NAStool/nas-tools/master/package_list.txt)) \ + && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo "${TZ}" > /etc/timezone \ + && ln -sf /usr/bin/python3 /usr/bin/python \ + && curl https://rclone.org/install.sh | bash \ + && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ + && curl https://dl.min.io/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ + && chmod +x /usr/bin/mc \ + && pip install --upgrade pip setuptools wheel \ + && pip install cython \ + && pip install -r https://raw.githubusercontent.com/NAStool/nas-tools/master/requirements.txt \ + && apk del libffi-dev \ + && npm install pm2 -g \ + && rm -rf /tmp/* /root/.cache /var/cache/apk/* +ENV LANG="C.UTF-8" \ + TZ="Asia/Shanghai" \ + NASTOOL_CONFIG="/config/config.yaml" \ + NASTOOL_AUTO_UPDATE=true \ + NASTOOL_CN_UPDATE=true \ + NASTOOL_VERSION=master \ + PS1="\u@\h:\w \$ " \ + REPO_URL="https://github.com/NAStool/nas-tools.git" \ + PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ + ALPINE_MIRROR="mirrors.ustc.edu.cn" \ + PUID=0 \ + PGID=0 \ + UMASK=000 \ + WORKDIR="/nas-tools" +WORKDIR ${WORKDIR} +RUN python_ver=$(python3 -V | awk '{print $2}') \ + && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ + && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ + && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ + && git config --global pull.ff only \ + && git clone -b master ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ + && git config --global --add safe.directory ${WORKDIR} +EXPOSE 3000 +VOLUME ["/config"] +ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] \ No newline at end of file diff --git a/docker/Dockerfile.beta b/docker/Dockerfile.beta new file mode 100644 index 0000000..fafe141 --- /dev/null +++ b/docker/Dockerfile.beta @@ -0,0 +1,41 @@ +FROM alpine +RUN apk add --no-cache libffi-dev \ + && apk add --no-cache $(echo $(wget --no-check-certificate -qO- https://raw.githubusercontent.com/NAStool/nas-tools/dev/package_list.txt)) \ + && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo "${TZ}" > /etc/timezone \ + && ln -sf /usr/bin/python3 /usr/bin/python \ + && curl https://rclone.org/install.sh | bash \ + && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ + && curl https://dl.min.io/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ + && chmod +x /usr/bin/mc \ + && pip install --upgrade pip setuptools wheel \ + && pip install cython \ + && pip install -r https://raw.githubusercontent.com/NAStool/nas-tools/dev/requirements.txt \ + && apk del libffi-dev \ + && npm install pm2 -g \ + && rm -rf /tmp/* /root/.cache /var/cache/apk/* +ENV LANG="C.UTF-8" \ + TZ="Asia/Shanghai" \ + NASTOOL_CONFIG="/config/config.yaml" \ + NASTOOL_AUTO_UPDATE=true \ + NASTOOL_CN_UPDATE=true \ + NASTOOL_VERSION=dev \ + PS1="\u@\h:\w \$ " \ + REPO_URL="https://github.com/NAStool/nas-tools.git" \ + PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ + ALPINE_MIRROR="mirrors.ustc.edu.cn" \ + PUID=0 \ + PGID=0 \ + UMASK=000 \ + WORKDIR="/nas-tools" +WORKDIR ${WORKDIR} +RUN python_ver=$(python3 -V | awk '{print $2}') \ + && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ + && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ + && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ + && git config --global pull.ff only \ + && git clone -b dev ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ + && git config --global --add safe.directory ${WORKDIR} +EXPOSE 3000 +VOLUME ["/config"] +ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] diff --git a/docker/Dockerfile.lite b/docker/Dockerfile.lite new file mode 100644 index 0000000..023172a --- /dev/null +++ b/docker/Dockerfile.lite @@ -0,0 +1,48 @@ +FROM alpine +RUN apk add --no-cache libffi-dev \ + git \ + gcc \ + musl-dev \ + python3-dev \ + py3-pip \ + libxml2-dev \ + libxslt-dev \ + tzdata \ + su-exec \ + dumb-init \ + npm \ + && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo "${TZ}" > /etc/timezone \ + && ln -sf /usr/bin/python3 /usr/bin/python \ + && pip install --upgrade pip setuptools wheel \ + && pip install cython \ + && pip install -r https://raw.githubusercontent.com/NAStool/nas-tools/master/requirements.txt \ + && npm install pm2 -g \ + && apk del --purge libffi-dev gcc musl-dev libxml2-dev libxslt-dev \ + && pip uninstall -y cython \ + && rm -rf /tmp/* /root/.cache /var/cache/apk/* +ENV LANG="C.UTF-8" \ + TZ="Asia/Shanghai" \ + NASTOOL_CONFIG="/config/config.yaml" \ + NASTOOL_AUTO_UPDATE=false \ + NASTOOL_CN_UPDATE=true \ + NASTOOL_VERSION=lite \ + PS1="\u@\h:\w \$ " \ + REPO_URL="https://github.com/NAStool/nas-tools.git" \ + PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ + ALPINE_MIRROR="mirrors.ustc.edu.cn" \ + PUID=0 \ + PGID=0 \ + UMASK=000 \ + WORKDIR="/nas-tools" +WORKDIR ${WORKDIR} +RUN python_ver=$(python3 -V | awk '{print $2}') \ + && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ + && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ + && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ + && git config --global pull.ff only \ + && git clone -b master ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ + && git config --global --add safe.directory ${WORKDIR} +EXPOSE 3000 +VOLUME ["/config"] +ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] \ No newline at end of file diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..18e2eb8 --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,19 @@ +version: "3" +services: + nas-tools: + image: jxxghp/nas-tools:latest + ports: + - 3000:3000 # 默认的webui控制端口 + volumes: + - ./config:/config # 冒号左边请修改为你想保存配置的路径 + - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 + environment: + - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid + - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid + - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 + - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true + #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 + restart: always + network_mode: bridge + hostname: nas-tools + container_name: nas-tools \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..d6fd324 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +cd ${WORKDIR} +if [ "${NASTOOL_AUTO_UPDATE}" = "true" ]; then + if [ ! -s /tmp/requirements.txt.sha256sum ]; then + sha256sum requirements.txt > /tmp/requirements.txt.sha256sum + fi + if [ ! -s /tmp/third_party.txt.sha256sum ]; then + sha256sum third_party.txt > /tmp/third_party.txt.sha256sum + fi + if [ "${NASTOOL_VERSION}" != "lite" ]; then + if [ ! -s /tmp/package_list.txt.sha256sum ]; then + sha256sum package_list.txt > /tmp/package_list.txt.sha256sum + fi + fi + echo "更新程序..." + git remote set-url origin "${REPO_URL}" &> /dev/null + echo "windows/" > .gitignore + if [ "${NASTOOL_VERSION}" == "dev" ]; then + branch="dev" + else + branch="master" + fi + git clean -dffx + git fetch --depth 1 origin ${branch} + git reset --hard origin/${branch} + if [ $? -eq 0 ]; then + echo "更新成功..." + # Python依赖包更新 + hash_old=$(cat /tmp/requirements.txt.sha256sum) + hash_new=$(sha256sum requirements.txt) + if [ "${hash_old}" != "${hash_new}" ]; then + echo "检测到requirements.txt有变化,重新安装依赖..." + if [ "${NASTOOL_CN_UPDATE}" = "true" ]; then + pip install --upgrade pip setuptools wheel -i "${PYPI_MIRROR}" + pip install -r requirements.txt -i "${PYPI_MIRROR}" + else + pip install --upgrade pip setuptools wheel + pip install -r requirements.txt + fi + if [ $? -ne 0 ]; then + echo "无法安装依赖,请更新镜像..." + else + echo "依赖安装成功..." + sha256sum requirements.txt > /tmp/requirements.txt.sha256sum + hash_old=$(cat /tmp/third_party.txt.sha256sum) + hash_new=$(sha256sum third_party.txt) + if [ "${hash_old}" != "${hash_new}" ]; then + echo "检测到third_party.txt有变化,更新第三方组件..." + git submodule update --init --recursive + if [ $? -ne 0 ]; then + echo "无法更新第三方组件,请更新镜像..." + else + echo "第三方组件安装成功..." + sha256sum third_party.txt > /tmp/third_party.txt.sha256sum + fi + fi + fi + fi + # 系统软件包更新 + if [ "${NASTOOL_VERSION}" != "lite" ]; then + hash_old=$(cat /tmp/package_list.txt.sha256sum) + hash_new=$(sha256sum package_list.txt) + if [ "${hash_old}" != "${hash_new}" ]; then + echo "检测到package_list.txt有变化,更新软件包..." + if [ "${NASTOOL_CN_UPDATE}" = "true" ]; then + sed -i "s/dl-cdn.alpinelinux.org/${ALPINE_MIRROR}/g" /etc/apk/repositories + apk update -f + fi + apk add --no-cache libffi-dev + apk add --no-cache $(echo $(cat package_list.txt)) + if [ $? -ne 0 ]; then + echo "无法更新软件包,请更新镜像..." + else + apk del libffi-dev + echo "软件包安装成功..." + sha256sum package_list.txt > /tmp/package_list.txt.sha256sum + fi + fi + fi + else + echo "更新失败,继续使用旧的程序来启动..." + fi +else + echo "程序自动升级已关闭,如需自动升级请在创建容器时设置环境变量:NASTOOL_AUTO_UPDATE=true" +fi + +echo "以PUID=${PUID},PGID=${PGID}的身份启动程序..." + +if [ "${NASTOOL_VERSION}" = "lite" ]; then + mkdir -p /.pm2 + chown -R "${PUID}":"${PGID}" "${WORKDIR}" /config /.pm2 +else + mkdir -p /.local + mkdir -p /.pm2 + chown -R "${PUID}":"${PGID}" "${WORKDIR}" /config /usr/lib/chromium /.local /.pm2 + export PATH=${PATH}:/usr/lib/chromium +fi +umask "${UMASK}" +exec su-exec "${PUID}":"${PGID}" "$(which dumb-init)" "$(which pm2-runtime)" start run.py -n NAStool --interpreter python3 diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 0000000..c18713b --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,93 @@ +## 特点 + +- 基于alpine实现,镜像体积小; + +- 镜像层数少; + +- 支持 amd64/arm64 架构; + +- 重启即可更新程序,如果依赖有变化,会自动尝试重新安装依赖,若依赖自动安装不成功,会提示更新镜像; + +- 可以以非root用户执行任务,降低程序权限和潜在风险; + +- 可以设置文件掩码权限umask。 + +- lite 版本不包含浏览器内核及xvfb,不支持浏览器仿真;不支持Rclone/Minio转移方式;不支持复杂依赖变更时的自动安装升级;但是体积更小。 + +## 创建 + +**注意** + +- 媒体目录的设置必须符合 [配置说明](https://github.com/NAStool/nas-tools#%E9%85%8D%E7%BD%AE) 的要求。 + +- umask含义详见:http://www.01happy.com/linux-umask-analyze 。 + +- 创建后请根据 [配置说明](https://github.com/NAStool/nas-tools#%E9%85%8D%E7%BD%AE) 及该文件本身的注释,修改`config/config.yaml`,修改好后再重启容器,最后访问`http://:`。 + +**docker cli** + +``` +docker run -d \ + --name nas-tools \ + --hostname nas-tools \ + -p 3000:3000 `# 默认的webui控制端口` \ + -v $(pwd)/config:/config `# 冒号左边请修改为你想在主机上保存配置文件的路径` \ + -v /你的媒体目录:/你想设置的容器内能见到的目录 `# 媒体目录,多个目录需要分别映射进来` \ + -e PUID=0 `# 想切换为哪个用户来运行程序,该用户的uid,详见下方说明` \ + -e PGID=0 `# 想切换为哪个用户来运行程序,该用户的gid,详见下方说明` \ + -e UMASK=000 `# 掩码权限,默认000,可以考虑设置为022` \ + -e NASTOOL_AUTO_UPDATE=false `# 如需在启动容器时自动升级程程序请设置为true` \ + -e NASTOOL_CN_UPDATE=false `# 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新` \ + jxxghp/nas-tools +``` + +如果你访问github的网络不太好,可以考虑在创建容器时增加设置一个环境变量`-e REPO_URL="https://ghproxy.com/https://github.com/NAStool/nas-tools.git" \`。 + +**docker-compose** + +新建`docker-compose.yaml`文件如下,并以命令`docker-compose up -d`启动。 + +``` +version: "3" +services: + nas-tools: + image: jxxghp/nas-tools:latest + ports: + - 3000:3000 # 默认的webui控制端口 + volumes: + - ./config:/config # 冒号左边请修改为你想保存配置的路径 + - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 + environment: + - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid + - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid + - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 + - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true + - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新 + #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 + restart: always + network_mode: bridge + hostname: nas-tools + container_name: nas-tools +``` + +## 后续如何更新 + +- 正常情况下,如果设置了`NASTOOL_AUTO_UPDATE=true`,重启容器即可自动更新nas-tools程序。 + +- 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "更新失败,继续使用旧的程序来启动...",请再重启一次,如果一直都报此错误,请改善你的网络。 + +- 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "无法安装依赖,请更新镜像...",则需要删除旧容器,删除旧镜像,重新pull镜像,再重新创建容器。 + +## 关于PUID/PGID的说明 + +- 如在使用诸如emby、jellyfin、plex、qbittorrent、transmission、deluge、jackett、sonarr、radarr等等的docker镜像,请保证创建本容器时的PUID/PGID和它们一样。 + +- 在docker宿主上,登陆媒体文件所有者的这个用户,然后分别输入`id -u`和`id -g`可获取到uid和gid,分别设置为PUID和PGID即可。 + +- `PUID=0` `PGID=0`指root用户,它拥有最高权限,若你的媒体文件的所有者不是root,不建议设置为`PUID=0` `PGID=0`。 + +## 如果要硬连接如何映射 + +参考下图,由imogel@telegram制作。 + +![如何映射](volume.png) diff --git a/docker/volume.png b/docker/volume.png new file mode 100644 index 0000000..5d0d81f Binary files /dev/null and b/docker/volume.png differ diff --git a/log.py b/log.py new file mode 100644 index 0000000..b9ae918 --- /dev/null +++ b/log.py @@ -0,0 +1,113 @@ +import logging +import os +import re +import threading +import time +from collections import deque +from html import escape +from logging.handlers import RotatingFileHandler + +from config import Config + +logging.getLogger('werkzeug').setLevel(logging.ERROR) +lock = threading.Lock() +LOG_QUEUE = deque(maxlen=200) +LOG_INDEX = 0 + + +class Logger: + logger = None + __instance = {} + __config = None + + __loglevels = { + "info": logging.INFO, + "debug": logging.DEBUG, + "error": logging.ERROR + } + + def __init__(self, module): + self.logger = logging.getLogger(module) + self.__config = Config() + logtype = self.__config.get_config('app').get('logtype') or "console" + loglevel = self.__config.get_config('app').get('loglevel') or "info" + self.logger.setLevel(level=self.__loglevels.get(loglevel)) + if logtype == "server": + logserver = self.__config.get_config('app').get('logserver', '').split(':') + if logserver: + logip = logserver[0] + if len(logserver) > 1: + logport = int(logserver[1] or '514') + else: + logport = 514 + log_server_handler = logging.handlers.SysLogHandler((logip, logport), + logging.handlers.SysLogHandler.LOG_USER) + log_server_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s')) + self.logger.addHandler(log_server_handler) + elif logtype == "file": + # 记录日志到文件 + logpath = os.environ.get('NASTOOL_LOG') or self.__config.get_config('app').get('logpath') or "" + if logpath: + if not os.path.exists(logpath): + os.makedirs(logpath) + log_file_handler = RotatingFileHandler(filename=os.path.join(logpath, module + ".txt"), + maxBytes=5 * 1024 * 1024, + backupCount=3, + encoding='utf-8') + log_file_handler.setFormatter(logging.Formatter('%(asctime)s\t%(levelname)s: %(message)s')) + self.logger.addHandler(log_file_handler) + # 记录日志到终端 + log_console_handler = logging.StreamHandler() + log_console_handler.setFormatter(logging.Formatter('%(asctime)s\t%(levelname)s: %(message)s')) + self.logger.addHandler(log_console_handler) + + @staticmethod + def get_instance(module): + if not module: + module = "run" + if Logger.__instance.get(module): + return Logger.__instance.get(module) + with lock: + Logger.__instance[module] = Logger(module) + return Logger.__instance.get(module) + + +def __append_log_queue(level, text): + global LOG_INDEX, LOG_QUEUE + with lock: + text = escape(text) + if text.startswith("【"): + source = re.findall(r"(?<=【).*?(?=】)", text)[0] + text = text.replace(f"【{source}】", "") + else: + source = "System" + LOG_QUEUE.append({ + "time": time.strftime('%H:%M:%S', time.localtime(time.time())), + "level": level, + "source": source, + "text": text}) + LOG_INDEX += 1 + + +def debug(text, module=None): + return Logger.get_instance(module).logger.debug(text) + + +def info(text, module=None): + __append_log_queue("INFO", text) + return Logger.get_instance(module).logger.info(text) + + +def error(text, module=None): + __append_log_queue("ERROR", text) + return Logger.get_instance(module).logger.error(text) + + +def warn(text, module=None): + __append_log_queue("WARN", text) + return Logger.get_instance(module).logger.warning(text) + + +def console(text): + __append_log_queue("INFO", text) + print(text) diff --git a/package_list.txt b/package_list.txt new file mode 100644 index 0000000..153cc56 --- /dev/null +++ b/package_list.txt @@ -0,0 +1,19 @@ +git +gcc +musl-dev +python3-dev +py3-pip +libxml2-dev +libxslt-dev +tzdata +su-exec +zip +curl +bash +fuse +xvfb +inotify-tools +chromium-chromedriver +npm +dumb-init +ffmpeg \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cbf82ec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,104 @@ +alembic==1.8.1 +aniso8601==9.0.1 +APScheduler==3.9.1 +asttokens==2.0.8 +async-generator==1.10 +attrs==22.1.0 +backcall==0.2.0 +backports.shutil-get-terminal-size==1.0.0 +beautifulsoup4==4.11.1 +better-exceptions==0.3.3 +bs4==0.0.1 +cacheout==0.14.1 +certifi==2022.6.15 +cffi==1.15.1 +charset-normalizer==2.1.1 +click==8.1.3 +cn2an==0.5.17 +colorama==0.4.4 +colored==1.3.93 +cssselect==1.1.0 +DBUtils==3.0.2 +dateparser==1.1.4 +decorator==5.1.1 +executing==1.1.0 +Flask==2.1.2 +Flask-Login==0.6.2 +fast-bencode==1.1.3 +flask-compress==1.13 +flask-restx==0.5.1 +greenlet==1.1.3.post0 +h11==0.12.0 +humanize==4.4.0 +idna==3.3 +influxdb==5.3.1 +itsdangerous==2.1.2 +jedi==0.18.1 +Jinja2==3.1.2 +jsonpath==0.82 +jsonschema==4.16.0 +loguru==0.6.0 +lxml==4.9.1 +Mako==1.2.3 +MarkupSafe==2.1.1 +matplotlib-inline==0.1.6 +msgpack==1.0.4 +outcome==1.2.0 +parse==1.19.0 +parsel==1.6.0 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +pikpakapi==0.1.1 +proces==0.1.2 +prompt-toolkit==3.0.31 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycparser==2.21 +pycryptodome==3.15.0 +Pygments==2.13.0 +PyJWT==2.5.0 +pymongo==4.2.0 +PyMySQL==1.0.2 +pyperclip==1.8.2 +pypushdeer==0.0.3 +pyquery==1.4.3 +pyrsistent==0.18.1 +PySocks==1.7.1 +python-dateutil==2.8.2 +python-dotenv==0.20.0 +pytz==2022.2.1 +pytz-deprecation-shim==0.1.0.post0 +PyVirtualDisplay==3.0 +redis==3.5.3 +redis-py-cluster==2.1.3 +regex==2022.9.13 +requests==2.28.1 +ruamel.yaml==0.17.21 +ruamel.yaml.clib==0.2.7 +selenium==4.4.3 +six==1.16.0 +slack-sdk==3.19.5 +sniffio==1.2.0 +sortedcontainers==2.4.0 +soupsieve==2.3.2.post1 +SQLAlchemy==1.4.42 +stack-data==0.5.1 +terminal-layout==2.1.2 +tqdm==4.64.0 +traitlets==5.4.0 +trio==0.21.0 +trio-websocket==0.9.2 +typing_extensions==4.3.0 +tzdata==2022.2 +tzlocal==4.2 +undetected-chromedriver==3.1.7 +urllib3==1.26.12 +w3lib==2.0.1 +watchdog==2.1.9 +wcwidth==0.2.5 +webdriver-manager==3.8.5 +websockets==10.3 +Werkzeug==2.1.2 +wsproto==1.2.0 +zhconv==1.4.3 diff --git a/run.py b/run.py new file mode 100644 index 0000000..465d3de --- /dev/null +++ b/run.py @@ -0,0 +1,199 @@ +import os +import signal +import sys +import time +import warnings + +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +warnings.filterwarnings('ignore') + +# 运行环境判断 +is_windows_exe = getattr(sys, 'frozen', False) and (os.name == "nt") +if is_windows_exe: + # 托盘相关库 + import threading + from windows.trayicon import TrayIcon, NullWriter + + # 初始化环境变量 + os.environ["NASTOOL_CONFIG"] = os.path.join(os.path.dirname(sys.executable), + "config", + "config.yaml").replace("\\", "/") + os.environ["NASTOOL_LOG"] = os.path.join(os.path.dirname(sys.executable), + "config", + "logs").replace("\\", "/") + try: + config_dir = os.path.join(os.path.dirname(sys.executable), + "config").replace("\\", "/") + if not os.path.exists(config_dir): + os.makedirs(config_dir) + except Exception as err: + print(str(err)) + +from config import Config +import log +from web.main import App +from app.utils import SystemUtils, ConfigLoadCache +from app.utils.commons import INSTANCES +from app.db import init_db, update_db, init_data +from app.helper import IndexerHelper, DisplayHelper, ChromeHelper +from app.brushtask import BrushTask +from app.rsschecker import RssChecker +from app.scheduler import run_scheduler, restart_scheduler +from app.sync import run_monitor, restart_monitor +from app.torrentremover import TorrentRemover +from app.speedlimiter import SpeedLimiter +from check_config import update_config, check_config +from version import APP_VERSION + + +def sigal_handler(num, stack): + """ + 信号处理 + """ + if SystemUtils.is_docker(): + log.warn('捕捉到退出信号:%s,开始退出...' % num) + # 停止虚拟显示 + DisplayHelper().quit() + # 退出主进程 + sys.exit() + + +def get_run_config(): + """ + 获取运行配置 + """ + _web_host = "::" + _web_port = 3000 + _ssl_cert = None + _ssl_key = None + _debug = False + + app_conf = Config().get_config('app') + if app_conf: + if app_conf.get("web_host"): + _web_host = app_conf.get("web_host").replace('[', '').replace(']', '') + _web_port = int(app_conf.get('web_port')) if str(app_conf.get('web_port', '')).isdigit() else 3000 + _ssl_cert = app_conf.get('ssl_cert') + _ssl_key = app_conf.get('ssl_key') + _ssl_key = app_conf.get('ssl_key') + _debug = True if app_conf.get("debug") else False + + app_arg = dict(host=_web_host, port=_web_port, debug=_debug, threaded=True, use_reloader=False) + if _ssl_cert: + app_arg['ssl_context'] = (_ssl_cert, _ssl_key) + return app_arg + + +# 退出事件 +signal.signal(signal.SIGINT, sigal_handler) +signal.signal(signal.SIGTERM, sigal_handler) + + +def init_system(): + # 配置 + log.console('NAStool 当前版本号:%s' % APP_VERSION) + # 数据库初始化 + init_db() + # 数据库更新 + update_db() + # 数据初始化 + init_data() + # 升级配置文件 + update_config() + # 检查配置文件 + check_config() + + +def start_service(): + log.console("开始启动服务...") + # 加载索引器配置 + IndexerHelper() + # 启动虚拟显示 + DisplayHelper() + # 启动定时服务 + run_scheduler() + # 启动监控服务 + run_monitor() + # 启动刷流服务 + BrushTask() + # 启动自定义订阅服务 + RssChecker() + # 启动自动删种服务 + TorrentRemover() + # 启动播放限速服务 + SpeedLimiter() + # 初始化浏览器驱动 + if not is_windows_exe: + ChromeHelper().init_driver() + + +def monitor_config(): + class _ConfigHandler(FileSystemEventHandler): + """ + 配置文件变化响应 + """ + + def __init__(self): + FileSystemEventHandler.__init__(self) + + def on_modified(self, event): + if not event.is_directory \ + and os.path.basename(event.src_path) == "config.yaml": + # 10秒内只能加载一次 + if ConfigLoadCache.get(event.src_path): + return + ConfigLoadCache.set(event.src_path, True) + log.console("进程 %s 检测到配置文件已修改,正在重新加载..." % os.getpid()) + time.sleep(1) + # 重新加载配置 + Config().init_config() + # 重载singleton服务 + for instance in INSTANCES.values(): + if hasattr(instance, "init_config"): + instance.init_config() + # 重启定时服务 + restart_scheduler() + # 重启监控服务 + restart_monitor() + + # 配置文件监听 + _observer = Observer(timeout=10) + _observer.schedule(_ConfigHandler(), path=Config().get_config_path(), recursive=False) + _observer.daemon = True + _observer.start() + + +# 系统初始化 +init_system() + +# 启动服务 +start_service() + +# 监听配置文件变化 +monitor_config() + +# 本地运行 +if __name__ == '__main__': + # Windows启动托盘 + if is_windows_exe: + homepage = Config().get_config('app').get('domain') + if not homepage: + homepage = "http://localhost:%s" % str(Config().get_config('app').get('web_port')) + log_path = os.environ.get("NASTOOL_LOG") + + sys.stdout = NullWriter() + sys.stderr = NullWriter() + + + def traystart(): + TrayIcon(homepage, log_path) + + + if len(os.popen("tasklist| findstr %s" % os.path.basename(sys.executable), 'r').read().splitlines()) <= 2: + p1 = threading.Thread(target=traystart, daemon=True) + p1.start() + + # gunicorn 启动 + App.run(**get_run_config()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/__init__.py b/tests/cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/meta_cases.py b/tests/cases/meta_cases.py new file mode 100644 index 0000000..f8001bc --- /dev/null +++ b/tests/cases/meta_cases.py @@ -0,0 +1,913 @@ +meta_cases = [{ + "title": "【爪爪字幕组】★7月新番[欢迎来到实力至上主义的教室 第二季/Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e S2][11][1080p][HEVC][GB][MP4][招募翻译校对]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Youkoso Jitsuryoku Shijou Shugi No Kyoushitsu E", + "year": "", + "part": "", + "season": "S02", + "episode": "E11", + "restype": "", + "pix": "1080p", + "video_codec": "HEVC", + "audio_codec": "" + } +}, { + "title": "National.Parks.Adventure.AKA.America.Wild:.National.Parks.Adventure.3D.2016.1080p.Blu-ray.AVC.TrueHD.7.1", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "National Parks Adventure", + "year": "2016", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay 3D", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "TrueHD 7.1" + } +}, { + "title": "[秋叶原冥途战争][Akiba Maid Sensou][2022][WEB-DL][1080][TV Series][第01话][LeagueWEB]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Akiba Maid Sensou", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E01", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "哆啦A梦:大雄的宇宙小战争 2021 (2022) - 1080p.mp4", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "哆啦A梦:大雄的宇宙小战争 2021", + "en_name": "", + "year": "2022", + "part": "", + "season": "", + "episode": "", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "新精武门1991 (1991).mkv", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "新精武门1991", + "en_name": "", + "year": "1991", + "part": "", + "season": "", + "episode": "", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "24 S01 1080p WEB-DL AAC2.0 H.264-BTN", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "24", + "year": "", + "part": "", + "season": "S01", + "episode": "", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC 2.0" + } +}, { + "title": "Qi Refining for 3000 Years S01E06 2022 1080p B-Blobal WEB-DL X264 AAC-AnimeS@AdWeb", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Qi Refining For 3000 Years", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E06", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "AAC" + } +}, { + "title": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta S01E02 2022 1080p B-Global WEB-DL X264 AAC-AnimeS@ADWeb[2022年10月新番]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Noumin Kanren No Skill Bakka Agetetara Naze Ka Tsuyoku Natta", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E02", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "AAC" + } +}, { + "title": "dou luo da lu S01E229 2018 2160p WEB-DL H265 AAC-ADWeb[[国漫连载] 斗罗大陆 第229集 4k | 国语中字]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Dou Luo Da Lu", + "year": "2018", + "part": "", + "season": "S01", + "episode": "E229", + "restype": "WEB-DL", + "pix": "2160p", + "video_codec": "H265", + "audio_codec": "AAC" + } +}, { + "title": "Thor Love and Thunder (2022) [1080p] [WEBRip] [5.1]", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "Thor Love And Thunder", + "year": "2022", + "part": "", + "season": "", + "episode": "", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "5.1" + } +}, { + "title": "[Animations(动画片)][[诛仙][Jade Dynasty][2022][WEB-DL][2160][TV Series][TV 08][LeagueWEB]][诛仙/诛仙动画 第一季 第08集 | 类型:动画 [国语中字]][680.12 MB]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Jade Dynasty", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E08", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "钢铁侠2 (2010) 1080p AC3.mp4", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "钢铁侠2", + "en_name": "", + "year": "2010", + "part": "", + "season": "", + "episode": "", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "AC3" + } +}, { + "title": "Wonder Woman 1984 2020 BluRay 1080p Atmos TrueHD 7.1 X264-EPiC", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "Wonder Woman 1984", + "year": "2020", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "Atmos TrueHD 7.1" + } +}, { + "title": "9-1-1 - S04E03 - Future Tense WEBDL-1080p.mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "9 1 1", + "year": "", + "part": "", + "season": "S04", + "episode": "E03", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "【幻月字幕组】【22年日剧】【据幸存的六人所说】【04】【1080P】【中日双语】", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "据幸存的六人所说", + "en_name": "", + "year": "", + "part": "", + "season": "S01", + "episode": "E04", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "【爪爪字幕组】★7月新番[即使如此依旧步步进逼/Soredemo Ayumu wa Yosetekuru][09][1080p][HEVC][GB][MP4][招募翻译校对]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Soredemo Ayumu Wa Yosetekuru", + "year": "", + "part": "", + "season": "S01", + "episode": "E09", + "restype": "", + "pix": "1080p", + "video_codec": "HEVC", + "audio_codec": "" + } +}, { + "title": "[猎户不鸽发布组] 不死者之王 第四季 OVERLORD Ⅳ [02] [1080p] [简中内封] [2022年7月番]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "不死者之王", + "en_name": "Overlord Ⅳ", + "year": "", + "part": "", + "season": "S04", + "episode": "E02", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "[GM-Team][国漫][寻剑 第1季][Sword Quest Season 1][2002][02][AVC][GB][1080P]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Sword Quest", + "year": "2002", + "part": "", + "season": "S01", + "episode": "E02", + "restype": "", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "" + } +}, { + "title": " [猎户不鸽发布组] 组长女儿与照料专员 / 组长女儿与保姆 Kumichou Musume to Sewagakari [09] [1080p+] [简中内嵌] [2022年7月番]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "组长女儿与保姆", + "en_name": "Kumichou Musume To Sewagakari", + "year": "", + "part": "", + "season": "S01", + "episode": "E09", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "Nande Koko ni Sensei ga!? 2019 Blu-ray Remux 1080p AVC LPCM-7³ ACG", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "Nande Koko Ni Sensei Ga!?", + "year": "2019", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay Remux", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "LPCM 7³" + } +}, { + "title": "30.Rock.S02E01.1080p.BluRay.X264-BORDURE.mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "30 Rock", + "year": "", + "part": "", + "season": "S02", + "episode": "E01", + "restype": "BluRay", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "" + } +}, { + "title": "[Gal to Kyouryuu][02][BDRIP][1080P][H264_FLAC].mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Gal To Kyouryuu", + "year": "", + "part": "", + "season": "S01", + "episode": "E02", + "restype": "", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "FLAC" + } +}, { + "title": "[AI-Raws] 逆境無頼カイジ #13 (BD HEVC 1920x1080 yuv444p10le FLAC)[7CFEE642].mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "逆境無頼カイジ", + "en_name": "", + "year": "", + "part": "", + "season": "S01", + "episode": "E13", + "restype": "BD", + "pix": "1080p", + "video_codec": "HEVC", + "audio_codec": "FLAC" + } +}, { + "title": "Mr. Robot - S02E06 - eps2.4_m4ster-s1ave.aes SDTV.mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Mr Robot", + "year": "", + "part": "", + "season": "S02", + "episode": "E06", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "[神印王座][Throne of Seal][2022][WEB-DL][2160][TV Series][TV 22][LeagueWEB] 神印王座 第一季 第22集 | 类型:动画 [国语中字][967.44 MB]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Throne Of Seal", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E22", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "S02E1000.mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "", + "year": "", + "part": "", + "season": "S02", + "episode": "E1000", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "西部世界 12.mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "西部世界", + "en_name": "", + "year": "", + "part": "", + "season": "S01", + "episode": "E12", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "[ANi] OVERLORD 第四季 - 04 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Overlord", + "year": "", + "part": "", + "season": "S04", + "episode": "E04", + "restype": "", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "AAC" + } +}, { + "title": "[SweetSub&LoliHouse] Made in Abyss S2 - 03v2 [WebRip 1080p HEVC-10bit AAC ASSx2].mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Made In Abyss", + "year": "", + "part": "", + "season": "S02", + "episode": "E03", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "AAC" + } +}, { + "title": "[GM-Team][国漫][斗破苍穹 第5季][Fights Break Sphere V][2022][05][HEVC][GB][4K]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Fights Break Sphere V", + "year": "2022", + "part": "", + "season": "S05", + "episode": "E05", + "restype": "", + "pix": "2160p", + "video_codec": "HEVC", + "audio_codec": "" + } +}, { + "title": "Ousama Ranking S01E02-[1080p][BDRIP][X265.FLAC].mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Ousama Ranking", + "year": "", + "part": "", + "season": "S01", + "episode": "E02", + "restype": "BDRIP", + "pix": "1080p", + "video_codec": "X265", + "audio_codec": "FLAC" + } +}, { + "title": "[Nekomoe kissaten&LoliHouse] Soredemo Ayumu wa Yosetekuru - 01v2 [WebRip 1080p HEVC-10bit EAC3 ASSx2].mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Soredemo Ayumu Wa Yosetekuru", + "year": "", + "part": "", + "season": "S01", + "episode": "E01", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "EAC3" + } +}, { + "title": "[喵萌奶茶屋&LoliHouse] 金装的薇尔梅 / Kinsou no Vermeil - 01 [WebRip 1080p HEVC-10bit AAC][简繁内封字幕]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Kinsou No Vermeil", + "year": "", + "part": "", + "season": "S01", + "episode": "E01", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "AAC" + } +}, { + "title": "Hataraku.Maou-sama.S02E05.2022.1080p.CR.WEB-DL.X264.AAC-ADWeb.mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Hataraku Maou Sama", + "year": "2022", + "part": "", + "season": "S02", + "episode": "E05", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "AAC" + } +}, { + "title": "The Witch Part 2:The Other One 2022 1080p WEB-DL AAC5.1 H264-tG1R0", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "The Witch Part 2:The Other One", + "year": "2022", + "part": "", + "season": "", + "episode": "", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC 5.1" + } +}, { + "title": "一夜新娘 - S02E07 - 第 7 集.mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "一夜新娘", + "en_name": "", + "year": "", + "part": "", + "season": "S02", + "episode": "E07", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "[ANi] 處刑少女的生存之道 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "处刑少女的生存之道", + "en_name": "", + "year": "", + "part": "", + "season": "S01", + "episode": "E07", + "restype": "", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "AAC" + } +}, { + "title": "Stand-up.Comedy.S01E01.PartA.2022.1080p.WEB-DL.H264.AAC-TJUPT.mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Stand Up Comedy", + "year": "2022", + "part": "PartA", + "season": "S01", + "episode": "E01", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC" + } +}, { + "title": "教父3.The.Godfather.Part.III.1990.1080p.NF.WEBRip.H264.DDP5.1-PTerWEB.mkv", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "教父3", + "en_name": "The Godfather Part Iii", + "year": "1990", + "part": "", + "season": "", + "episode": "", + "restype": "WEBRip", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "DDP 5.1" + } +}, { + "title": "A.Quiet.Place.Part.II.2020.1080p.UHD.BluRay.DD+7.1.DoVi.X265-PuTao", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "A Quiet Place Part Ii", + "year": "2020", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay DoVi UHD", + "pix": "1080p", + "video_codec": "X265", + "audio_codec": "DD 7.1" + } +}, { + "title": "Childhood.In.A.Capsule.S01E16.2022.1080p.KKTV.WEB-DL.X264.AAC-ADWeb.mkv", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Childhood In A Capsule", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E16", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "AAC" + } +}, { + "title": "[桜都字幕组] 异世界归来的舅舅 / Isekai Ojisan [01][1080p][简体内嵌]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Isekai Ojisan", + "year": "", + "part": "", + "season": "S01", + "episode": "E01", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "【喵萌奶茶屋】★04月新番★[夏日重現/Summer Time Rendering][15][720p][繁日雙語][招募翻譯片源]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Summer Time Rendering", + "year": "", + "part": "", + "season": "S01", + "episode": "E15", + "restype": "", + "pix": "720p", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "[NC-Raws] 打工吧!魔王大人 第二季 / Hataraku Maou-sama!! - 02 (B-Global 1920x1080 HEVC AAC MKV)", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Hataraku Maou-Sama!!", + "year": "", + "part": "", + "season": "S02", + "episode": "E02", + "restype": "", + "pix": "1080p", + "video_codec": "HEVC", + "audio_codec": "AAC" + } +}, { + "title": "The Witch Part 2 The Other One 2022 1080p WEB-DL AAC5.1 H.264-tG1R0", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "The Witch Part 2 The Other One", + "year": "2022", + "part": "", + "season": "", + "episode": "", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC 5.1" + } +}, { + "title": "The 355 2022 BluRay 1080p DTS-HD MA5.1 X265.10bit-BeiTai", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "The 355", + "year": "2022", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay", + "pix": "1080p", + "video_codec": "X265 10bit", + "audio_codec": "DTS-HD MA 5.1" + } +}, { + "title": "Sense8 s01-s02 2015-2017 1080P WEB-DL X265 AC3£cXcY@FRDS", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Sense8", + "year": "2015", + "part": "", + "season": "S01-S02", + "episode": "", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X265", + "audio_codec": "" + } +}, { + "title": "The Heart of Genius S01 13-14 2022 1080p WEB-DL H264 AAC", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "The Heart Of Genius", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E13-E14", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC" + } +}, { + "title": "The Heart of Genius E13-14 2022 1080p WEB-DL H264 AAC", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "The Heart Of Genius", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E13-E14", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "H264", + "audio_codec": "AAC" + } +}, { + "title": "2022.8.2.Twelve.Monkeys.1995.GBR.4K.REMASTERED.BluRay.1080p.X264.DTS [3.4 GB]", + "subtitle": "", + "target": { + "type": "电影", + "cn_name": "", + "en_name": "Twelve Monkeys", + "year": "1995", + "part": "", + "season": "", + "episode": "", + "restype": "BluRay", + "pix": "4k", + "video_codec": "X264", + "audio_codec": "DTS" + } +}, { + "title": "[NC-Raws] 王者天下 第四季 - 17 (Baha 1920x1080 AVC AAC MP4) [3B1AA7BB].mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "王者天下", + "en_name": "", + "year": "", + "part": "", + "season": "S04", + "episode": "E17", + "restype": "", + "pix": "1080p", + "video_codec": "AVC", + "audio_codec": "AAC" + } +}, { + "title": "Sense8 S2E1 2015-2017 1080P WEB-DL X265 AC3£cXcY@FRDS", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Sense8", + "year": "2015", + "part": "", + "season": "S02", + "episode": "E01", + "restype": "WEB-DL", + "pix": "1080p", + "video_codec": "X265", + "audio_codec": "" + } +}, { + "title": "[xyx98]传颂之物/Utawarerumono/うたわれるもの[BDrip][1920x1080][TV 01-26 Fin][hevc-yuv420p10 flac_ac3][ENG PGS]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "うたわれるもの", + "year": "", + "part": "", + "season": "S01", + "episode": "E01-E26", + "restype": "", + "pix": "1080p", + "video_codec": "", + "audio_codec": "flac" + } +}, { + "title": "[云歌字幕组][7月新番][欢迎来到实力至上主义的教室 第二季][01][X264 10bit][1080p][简体中文].mp4", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "欢迎来到实力至上主义的教室", + "en_name": "", + "year": "", + "part": "", + "season": "S02", + "episode": "E01", + "restype": "", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "" + } +}, { + "title": "[诛仙][Jade Dynasty][2022][WEB-DL][2160][TV Series][TV 04][LeagueWEB]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Jade Dynasty", + "year": "2022", + "part": "", + "season": "S01", + "episode": "E04", + "restype": "", + "pix": "", + "video_codec": "", + "audio_codec": "" + } +}, { + "title": "Rick and Morty.S06E06.JuRicksic.Mort.1080p.HMAX.WEBRip.DD5.1.X264-NTb[rartv]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Rick And Morty", + "year": "", + "part": "", + "season": "S06", + "episode": "E06", + "restype": "WEBRip", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "DD 5.1" + } +}, { + "title": "rick and Morty.S06E05.JuRicksic.Mort.1080p.HMAX.WEBRip.DD5.1.X264-NTb[rartv]", + "subtitle": "", + "target": { + "type": "电视剧", + "cn_name": "", + "en_name": "Rick And Morty", + "year": "", + "part": "", + "season": "S06", + "episode": "E05", + "restype": "WEBRip", + "pix": "1080p", + "video_codec": "X264", + "audio_codec": "DD 5.1" + } +}] diff --git a/tests/run.py b/tests/run.py new file mode 100644 index 0000000..4e0d2ef --- /dev/null +++ b/tests/run.py @@ -0,0 +1,12 @@ +import unittest + +from tests.test_metainfo import MetaInfoTest + +if __name__ == '__main__': + suite = unittest.TestSuite() + # 测试名称识别 + suite.addTest(MetaInfoTest('test_metainfo')) + + # 运行测试 + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/tests/test_metainfo.py b/tests/test_metainfo.py new file mode 100644 index 0000000..a638fe8 --- /dev/null +++ b/tests/test_metainfo.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase + +from app.media.meta import MetaInfo +from tests.cases.meta_cases import meta_cases + + +class MetaInfoTest(TestCase): + def setUp(self) -> None: + pass + + def tearDown(self) -> None: + pass + + def test_metainfo(self): + for info in meta_cases: + if not info.get("title"): + continue + meta_info = MetaInfo(title=info.get("title"), subtitle=info.get("subtitle")) + target = { + "type": meta_info.type.value, + "cn_name": meta_info.cn_name or "", + "en_name": meta_info.en_name or "", + "year": meta_info.year or "", + "part": meta_info.part or "", + "season": meta_info.get_season_string(), + "episode": meta_info.get_episode_string(), + "restype": meta_info.get_edtion_string(), + "pix": meta_info.resource_pix or "", + "video_codec": meta_info.video_encode or "", + "audio_codec": meta_info.audio_encode or "" + } + self.assertEqual(target, info.get("target")) diff --git a/third_party.txt b/third_party.txt new file mode 100644 index 0000000..e478a92 --- /dev/null +++ b/third_party.txt @@ -0,0 +1,6 @@ +feapder +qbittorrent-api +anitopy +plexapi +transmission-rpc +slack_bolt \ No newline at end of file diff --git a/version.py b/version.py new file mode 100644 index 0000000..59a6d97 --- /dev/null +++ b/version.py @@ -0,0 +1 @@ +APP_VERSION = 'v2.9.2' diff --git a/web/.DS_Store b/web/.DS_Store new file mode 100644 index 0000000..f477d08 Binary files /dev/null and b/web/.DS_Store differ diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/action.py b/web/action.py new file mode 100644 index 0000000..a6bf779 --- /dev/null +++ b/web/action.py @@ -0,0 +1,4565 @@ +import base64 +import datetime +import importlib +import json +import os.path +import re +import shutil +import signal +from math import floor +from urllib.parse import unquote + +import cn2an +from flask_login import logout_user, current_user +from werkzeug.security import generate_password_hash + +import log +from app.brushtask import BrushTask +from app.conf import SystemConfig, ModuleConf +from app.doubansync import DoubanSync +from app.downloader import Downloader +from app.downloader.client import Qbittorrent, Transmission +from app.filetransfer import FileTransfer +from app.filter import Filter +from app.helper import DbHelper, ProgressHelper, ThreadHelper, \ + MetaHelper, DisplayHelper, WordsHelper, CookieCloudHelper +from app.indexer import Indexer +from app.media import Category, Media, Bangumi, DouBan +from app.media.meta import MetaInfo, MetaBase +from app.mediaserver import MediaServer +from app.message import Message, MessageCenter +from app.rss import Rss +from app.rsschecker import RssChecker +from app.scheduler import stop_scheduler +from app.sites import Sites, SiteUserInfo, SiteSignin, SiteCookie +from app.subscribe import Subscribe +from app.subtitle import Subtitle +from app.sync import Sync, stop_monitor +from app.torrentremover import TorrentRemover +from app.speedlimiter import SpeedLimiter +from app.utils import StringUtils, EpisodeFormat, RequestUtils, PathUtils, \ + SystemUtils, ExceptionUtils, Torrent +from app.utils.types import RmtMode, OsType, SearchType, DownloaderType, SyncType, MediaType, MovieTypes, TvTypes +from config import RMT_MEDIAEXT, TMDB_IMAGE_W500_URL, RMT_SUBEXT, Config +from web.backend.search_torrents import search_medias_for_web, search_media_by_message +from web.backend.web_utils import WebUtils + + +class WebAction: + dbhelper = None + _actions = {} + TvTypes = ['TV', '电视剧'] + + def __init__(self): + self.dbhelper = DbHelper() + self._actions = { + "sch": self.__sch, + "search": self.__search, + "download": self.__download, + "download_link": self.__download_link, + "download_torrent": self.__download_torrent, + "pt_start": self.__pt_start, + "pt_stop": self.__pt_stop, + "pt_remove": self.__pt_remove, + "pt_info": self.__pt_info, + "del_unknown_path": self.__del_unknown_path, + "rename": self.__rename, + "rename_udf": self.__rename_udf, + "delete_history": self.__delete_history, + "logging": self.__logging, + "version": self.__version, + "update_site": self.__update_site, + "get_site": self.__get_site, + "del_site": self.__del_site, + "get_site_favicon": self.__get_site_favicon, + "restart": self.__restart, + "update_system": self.update_system, + "reset_db_version": self.__reset_db_version, + "logout": self.__logout, + "update_config": self.__update_config, + "update_directory": self.__update_directory, + "add_or_edit_sync_path": self.__add_or_edit_sync_path, + "get_sync_path": self.__get_sync_path, + "delete_sync_path": self.__delete_sync_path, + "check_sync_path": self.__check_sync_path, + "remove_rss_media": self.__remove_rss_media, + "add_rss_media": self.__add_rss_media, + "re_identification": self.re_identification, + "media_info": self.__media_info, + "test_connection": self.__test_connection, + "user_manager": self.__user_manager, + "refresh_rss": self.__refresh_rss, + "refresh_message": self.__refresh_message, + "delete_tmdb_cache": self.__delete_tmdb_cache, + "movie_calendar_data": self.__movie_calendar_data, + "tv_calendar_data": self.__tv_calendar_data, + "modify_tmdb_cache": self.__modify_tmdb_cache, + "rss_detail": self.__rss_detail, + "truncate_blacklist": self.truncate_blacklist, + "truncate_rsshistory": self.truncate_rsshistory, + "add_brushtask": self.__add_brushtask, + "del_brushtask": self.__del_brushtask, + "brushtask_detail": self.__brushtask_detail, + "add_downloader": self.__add_downloader, + "delete_downloader": self.__delete_downloader, + "get_downloader": self.__get_downloader, + "name_test": self.__name_test, + "rule_test": self.__rule_test, + "net_test": self.__net_test, + "add_filtergroup": self.__add_filtergroup, + "restore_filtergroup": self.__restore_filtergroup, + "set_default_filtergroup": self.__set_default_filtergroup, + "del_filtergroup": self.__del_filtergroup, + "add_filterrule": self.__add_filterrule, + "del_filterrule": self.__del_filterrule, + "filterrule_detail": self.__filterrule_detail, + "get_site_activity": self.__get_site_activity, + "get_site_history": self.__get_site_history, + "get_recommend": self.get_recommend, + "get_downloaded": self.get_downloaded, + "get_site_seeding_info": self.__get_site_seeding_info, + "clear_tmdb_cache": self.__clear_tmdb_cache, + "check_site_attr": self.__check_site_attr, + "refresh_process": self.__refresh_process, + "restory_backup": self.__restory_backup, + "start_mediasync": self.__start_mediasync, + "mediasync_state": self.__mediasync_state, + "get_tvseason_list": self.__get_tvseason_list, + "get_userrss_task": self.__get_userrss_task, + "delete_userrss_task": self.__delete_userrss_task, + "update_userrss_task": self.__update_userrss_task, + "get_rssparser": self.__get_rssparser, + "delete_rssparser": self.__delete_rssparser, + "update_rssparser": self.__update_rssparser, + "run_userrss": self.__run_userrss, + "run_brushtask": self.__run_brushtask, + "list_site_resources": self.__list_site_resources, + "list_rss_articles": self.__list_rss_articles, + "rss_article_test": self.__rss_article_test, + "list_rss_history": self.__list_rss_history, + "rss_articles_check": self.__rss_articles_check, + "rss_articles_download": self.__rss_articles_download, + "add_custom_word_group": self.__add_custom_word_group, + "delete_custom_word_group": self.__delete_custom_word_group, + "add_or_edit_custom_word": self.__add_or_edit_custom_word, + "get_custom_word": self.__get_custom_word, + "delete_custom_word": self.__delete_custom_word, + "check_custom_words": self.__check_custom_words, + "export_custom_words": self.__export_custom_words, + "analyse_import_custom_words_code": self.__analyse_import_custom_words_code, + "import_custom_words": self.__import_custom_words, + "get_categories": self.__get_categories, + "re_rss_history": self.__re_rss_history, + "delete_rss_history": self.__delete_rss_history, + "share_filtergroup": self.__share_filtergroup, + "import_filtergroup": self.__import_filtergroup, + "get_transfer_statistics": self.get_transfer_statistics, + "get_library_spacesize": self.get_library_spacesize, + "get_library_mediacount": self.get_library_mediacount, + "get_library_playhistory": self.get_library_playhistory, + "get_search_result": self.get_search_result, + "search_media_infos": self.search_media_infos, + "get_movie_rss_list": self.get_movie_rss_list, + "get_tv_rss_list": self.get_tv_rss_list, + "get_rss_history": self.get_rss_history, + "get_transfer_history": self.get_transfer_history, + "get_unknown_list": self.get_unknown_list, + "get_customwords": self.get_customwords, + "get_directorysync": self.get_directorysync, + "get_users": self.get_users, + "get_filterrules": self.get_filterrules, + "get_downloading": self.get_downloading, + "test_site": self.__test_site, + "get_sub_path": self.__get_sub_path, + "rename_file": self.__rename_file, + "delete_files": self.__delete_files, + "download_subtitle": self.__download_subtitle, + "get_download_setting": self.__get_download_setting, + "update_download_setting": self.__update_download_setting, + "delete_download_setting": self.__delete_download_setting, + "update_message_client": self.__update_message_client, + "delete_message_client": self.__delete_message_client, + "check_message_client": self.__check_message_client, + "get_message_client": self.__get_message_client, + "test_message_client": self.__test_message_client, + "get_sites": self.__get_sites, + "get_indexers": self.__get_indexers, + "get_download_dirs": self.__get_download_dirs, + "find_hardlinks": self.__find_hardlinks, + "update_sites_cookie_ua": self.__update_sites_cookie_ua, + "set_site_captcha_code": self.__set_site_captcha_code, + "update_torrent_remove_task": self.__update_torrent_remove_task, + "get_torrent_remove_task": self.__get_torrent_remove_task, + "delete_torrent_remove_task": self.__delete_torrent_remove_task, + "get_remove_torrents": self.__get_remove_torrents, + "auto_remove_torrents": self.__auto_remove_torrents, + "get_douban_history": self.get_douban_history, + "delete_douban_history": self.__delete_douban_history, + "list_brushtask_torrents": self.__list_brushtask_torrents, + "set_system_config": self.__set_system_config, + "get_site_user_statistics": self.get_site_user_statistics, + "send_custom_message": self.send_custom_message, + "cookiecloud_sync": self.__cookiecloud_sync, + "media_detail": self.media_detail, + "media_similar": self.__media_similar, + "media_recommendations": self.__media_recommendations, + "media_person": self.__media_person, + "person_medias": self.__person_medias, + "save_user_script": self.__save_user_script, + "run_directory_sync": self.__run_directory_sync + } + + def action(self, cmd, data=None): + func = self._actions.get(cmd) + if not func: + return {"code": -1, "msg": "非授权访问!"} + else: + return func(data) + + def api_action(self, cmd, data=None): + result = self.action(cmd, data) + if not result: + return { + "code": -1, + "success": False, + "message": "服务异常,未获取到返回结果" + } + code = result.get("code", result.get("retcode", 0)) + if not code or str(code) == "0": + success = True + else: + success = False + message = result.get("msg", result.get("retmsg", "")) + for key in ['code', 'retcode', 'msg', 'retmsg']: + if key in result: + result.pop(key) + return { + "code": code, + "success": success, + "message": message, + "data": result + } + + @staticmethod + def restart_server(): + """ + 停止进程 + """ + # 停止定时服务 + stop_scheduler() + # 停止监控 + stop_monitor() + # 签退 + logout_user() + # 关闭虚拟显示 + DisplayHelper().quit() + # 重启进程 + if os.name == "nt": + os.kill(os.getpid(), getattr(signal, "SIGKILL", signal.SIGTERM)) + elif SystemUtils.is_synology(): + os.system( + "ps -ef | grep -v grep | grep 'python run.py'|awk '{print $2}'|xargs kill -9") + else: + os.system("pm2 restart NAStool") + + @staticmethod + def handle_message_job(msg, in_from=SearchType.OT, user_id=None, user_name=None): + """ + 处理消息事件 + """ + if not msg: + return + commands = { + "/ptr": {"func": TorrentRemover().auto_remove_torrents, "desp": "删种"}, + "/ptt": {"func": Downloader().transfer, "desp": "下载文件转移"}, + "/pts": {"func": SiteSignin().signin, "desp": "站点签到"}, + "/rst": {"func": Sync().transfer_all_sync, "desp": "目录同步"}, + "/rss": {"func": Rss().rssdownload, "desp": "RSS订阅"}, + "/db": {"func": DoubanSync().sync, "desp": "豆瓣同步"}, + "/ssa": {"func": Subscribe().subscribe_search_all, "desp": "订阅搜索"}, + "/tbl": {"func": WebAction().truncate_blacklist, "desp": "清理转移缓存"}, + "/trh": {"func": WebAction().truncate_rsshistory, "desp": "清理RSS缓存"}, + "/utf": {"func": WebAction().unidentification, "desp": "重新识别"}, + "/udt": {"func": WebAction().update_system, "desp": "系统更新"} + } + command = commands.get(msg) + message = Message() + + if command: + # 启动服务 + ThreadHelper().start_thread(command.get("func"), ()) + message.send_channel_msg( + channel=in_from, title="正在运行 %s ..." % command.get("desp"), user_id=user_id) + else: + # 站点检索或者添加订阅 + ThreadHelper().start_thread(search_media_by_message, + (msg, in_from, user_id, user_name)) + + @staticmethod + def set_config_value(cfg, cfg_key, cfg_value): + """ + 根据Key设置配置值 + """ + # 密码 + if cfg_key == "app.login_password": + if cfg_value and not cfg_value.startswith("[hash]"): + cfg['app']['login_password'] = "[hash]%s" % generate_password_hash( + cfg_value) + else: + cfg['app']['login_password'] = cfg_value or "password" + return cfg + # 代理 + if cfg_key == "app.proxies": + if cfg_value: + if not cfg_value.startswith("http") and not cfg_value.startswith("sock"): + cfg['app']['proxies'] = { + "https": "http://%s" % cfg_value, "http": "http://%s" % cfg_value} + else: + cfg['app']['proxies'] = {"https": "%s" % + cfg_value, "http": "%s" % cfg_value} + else: + cfg['app']['proxies'] = {"https": None, "http": None} + return cfg + # 豆瓣用户列表 + if cfg_key == "douban.users": + vals = cfg_value.split(",") + cfg['douban']['users'] = vals + return cfg + # 最大支持三层赋值 + keys = cfg_key.split(".") + if keys: + if len(keys) == 1: + cfg[keys[0]] = cfg_value + elif len(keys) == 2: + if not cfg.get(keys[0]): + cfg[keys[0]] = {} + cfg[keys[0]][keys[1]] = cfg_value + elif len(keys) == 3: + if cfg.get(keys[0]): + if not cfg[keys[0]].get(keys[1]) or isinstance(cfg[keys[0]][keys[1]], str): + cfg[keys[0]][keys[1]] = {} + cfg[keys[0]][keys[1]][keys[2]] = cfg_value + else: + cfg[keys[0]] = {} + cfg[keys[0]][keys[1]] = {} + cfg[keys[0]][keys[1]][keys[2]] = cfg_value + + return cfg + + @staticmethod + def set_config_directory(cfg, oper, cfg_key, cfg_value, update_value=None): + """ + 更新目录数据 + """ + + def remove_sync_path(obj, key): + if not isinstance(obj, list): + return [] + ret_obj = [] + for item in obj: + if item.split("@")[0].replace("\\", "/") != key.split("@")[0].replace("\\", "/"): + ret_obj.append(item) + return ret_obj + + # 最大支持二层赋值 + keys = cfg_key.split(".") + if keys: + if len(keys) == 1: + if cfg.get(keys[0]): + if not isinstance(cfg[keys[0]], list): + cfg[keys[0]] = [cfg[keys[0]]] + if oper == "add": + cfg[keys[0]].append(cfg_value) + elif oper == "sub": + cfg[keys[0]].remove(cfg_value) + if not cfg[keys[0]]: + cfg[keys[0]] = None + elif oper == "set": + cfg[keys[0]].remove(cfg_value) + if update_value: + cfg[keys[0]].append(update_value) + else: + cfg[keys[0]] = cfg_value + elif len(keys) == 2: + if cfg.get(keys[0]): + if not cfg[keys[0]].get(keys[1]): + cfg[keys[0]][keys[1]] = [] + if not isinstance(cfg[keys[0]][keys[1]], list): + cfg[keys[0]][keys[1]] = [cfg[keys[0]][keys[1]]] + if oper == "add": + cfg[keys[0]][keys[1]].append( + cfg_value.replace("\\", "/")) + elif oper == "sub": + cfg[keys[0]][keys[1]] = remove_sync_path( + cfg[keys[0]][keys[1]], cfg_value) + if not cfg[keys[0]][keys[1]]: + cfg[keys[0]][keys[1]] = None + elif oper == "set": + cfg[keys[0]][keys[1]] = remove_sync_path( + cfg[keys[0]][keys[1]], cfg_value) + if update_value: + cfg[keys[0]][keys[1]].append( + update_value.replace("\\", "/")) + else: + cfg[keys[0]] = {} + cfg[keys[0]][keys[1]] = cfg_value.replace("\\", "/") + return cfg + + @staticmethod + def __sch(data): + """ + 启动定时服务 + """ + commands = { + "autoremovetorrents": TorrentRemover().auto_remove_torrents, + "pttransfer": Downloader().transfer, + "ptsignin": SiteSignin().signin, + "sync": Sync().transfer_all_sync, + "rssdownload": Rss().rssdownload, + "douban": DoubanSync().sync, + "subscribe_search_all": Subscribe().subscribe_search_all, + } + sch_item = data.get("item") + if sch_item and commands.get(sch_item): + ThreadHelper().start_thread(commands.get(sch_item), ()) + return {"retmsg": "服务已启动", "item": sch_item} + + @staticmethod + def __search(data): + """ + WEB检索资源 + """ + search_word = data.get("search_word") + ident_flag = False if data.get("unident") else True + filters = data.get("filters") + tmdbid = data.get("tmdbid") + media_type = data.get("media_type") + if media_type: + if media_type in MovieTypes: + media_type = MediaType.MOVIE + else: + media_type = MediaType.TV + if search_word: + ret, ret_msg = search_medias_for_web(content=search_word, + ident_flag=ident_flag, + filters=filters, + tmdbid=tmdbid, + media_type=media_type) + if ret != 0: + return {"code": ret, "msg": ret_msg} + return {"code": 0} + + def __download(self, data): + """ + 从WEB添加下载 + """ + dl_id = data.get("id") + dl_dir = data.get("dir") + dl_setting = data.get("setting") + results = self.dbhelper.get_search_result_by_id(dl_id) + for res in results: + media = Media().get_media_info(title=res.TORRENT_NAME, subtitle=res.DESCRIPTION) + if not media: + continue + media.set_torrent_info(enclosure=res.ENCLOSURE, + size=res.SIZE, + site=res.SITE, + page_url=res.PAGEURL, + upload_volume_factor=float( + res.UPLOAD_VOLUME_FACTOR), + download_volume_factor=float(res.DOWNLOAD_VOLUME_FACTOR)) + # 添加下载 + ret, ret_msg = Downloader().download(media_info=media, + download_dir=dl_dir, + download_setting=dl_setting) + if ret: + # 发送消息 + media.user_name = current_user.username + Message().send_download_message(in_from=SearchType.WEB, + can_item=media) + else: + return {"retcode": -1, "retmsg": ret_msg} + return {"retcode": 0, "retmsg": ""} + + @staticmethod + def __download_link(data): + """ + 从WEB添加下载链接 + """ + site = data.get("site") + enclosure = data.get("enclosure") + title = data.get("title") + description = data.get("description") + page_url = data.get("page_url") + size = data.get("size") + seeders = data.get("seeders") + uploadvolumefactor = data.get("uploadvolumefactor") + downloadvolumefactor = data.get("downloadvolumefactor") + dl_dir = data.get("dl_dir") + dl_setting = data.get("dl_setting") + if not title or not enclosure: + return {"code": -1, "msg": "种子信息有误"} + media = Media().get_media_info(title=title, subtitle=description) + media.site = site + media.enclosure = enclosure + media.page_url = page_url + media.size = size + media.upload_volume_factor = float(uploadvolumefactor) + media.download_volume_factor = float(downloadvolumefactor) + media.seeders = seeders + # 添加下载 + ret, ret_msg = Downloader().download(media_info=media, + download_dir=dl_dir, + download_setting=dl_setting) + if ret: + # 发送消息 + media.user_name = current_user.username + Message().send_download_message(SearchType.WEB, media) + return {"code": 0, "msg": "下载成功"} + else: + return {"code": 1, "msg": ret_msg or "如连接正常,请检查下载任务是否存在"} + + @staticmethod + def __download_torrent(data): + """ + 从种子文件添加下载 + """ + + def __download(_media_info, _file_path): + _media_info.site = "WEB" + # 添加下载 + ret, ret_msg = Downloader().download(media_info=_media_info, + download_dir=dl_dir, + download_setting=dl_setting, + torrent_file=_file_path) + # 发送消息 + _media_info.user_name = current_user.username + if ret: + Message().send_download_message(SearchType.WEB, _media_info) + else: + Message().send_download_fail_message(_media_info, ret_msg) + + dl_dir = data.get("dl_dir") + dl_setting = data.get("dl_setting") + files = data.get("files") + magnets = data.get("magnets") + if not files and not magnets: + return {"code": -1, "msg": "没有种子文件或磁链"} + for file_item in files: + if not file_item: + continue + file_name = file_item.get("upload", {}).get("filename") + file_path = os.path.join(Config().get_temp_path(), file_name) + media_info = Media().get_media_info(title=file_name) + __download(media_info, file_path) + for magnet in magnets: + if not magnet: + continue + file_path = None + title = Torrent().get_magnet_title(magnet) + if title: + media_info = Media().get_media_info(title=title) + else: + media_info = MetaInfo(title="磁力链接") + media_info.org_string = magnet + media_info.set_torrent_info(enclosure=magnet, + download_volume_factor=0, + upload_volume_factor=1) + __download(media_info, file_path) + return {"code": 0, "msg": "添加下载完成!"} + + @staticmethod + def __pt_start(data): + """ + 开始下载 + """ + tid = data.get("id") + if id: + Downloader().start_torrents(ids=tid) + return {"retcode": 0, "id": tid} + + @staticmethod + def __pt_stop(data): + """ + 停止下载 + """ + tid = data.get("id") + if id: + Downloader().stop_torrents(ids=tid) + return {"retcode": 0, "id": tid} + + @staticmethod + def __pt_remove(data): + """ + 删除下载 + """ + tid = data.get("id") + if id: + Downloader().delete_torrents(ids=tid, delete_file=True) + return {"retcode": 0, "id": tid} + + @staticmethod + def __pt_info(data): + """ + 查询具体种子的信息 + """ + ids = data.get("ids") + Client, Torrents = Downloader().get_torrents(torrent_ids=ids) + DispTorrents = [] + for torrent in Torrents: + if not torrent: + continue + if Client == DownloaderType.QB: + if torrent.get('state') in ['pausedDL']: + state = "Stoped" + speed = "已暂停" + else: + state = "Downloading" + dlspeed = StringUtils.str_filesize(torrent.get('dlspeed')) + eta = StringUtils.str_timelong(torrent.get('eta')) + upspeed = StringUtils.str_filesize(torrent.get('upspeed')) + speed = "%s%sB/s %s%sB/s %s" % (chr(8595), + dlspeed, chr(8593), upspeed, eta) + # 进度 + progress = round(torrent.get('progress') * 100) + # 主键 + key = torrent.get('hash') + elif Client == DownloaderType.Client115: + state = "Downloading" + dlspeed = StringUtils.str_filesize(torrent.get('peers')) + upspeed = StringUtils.str_filesize(torrent.get('rateDownload')) + speed = "%s%sB/s %s%sB/s" % (chr(8595), + dlspeed, chr(8593), upspeed) + # 进度 + progress = round(torrent.get('percentDone'), 1) + # 主键 + key = torrent.get('info_hash') + elif Client == DownloaderType.PikPak: + key = torrent.get('id') + if torrent.get('finish'): + speed = "PikPak: 下载完成" + else: + speed = "PikPak: 下载中" + state = "" + progress = "" + else: + if torrent.status in ['stopped']: + state = "Stoped" + speed = "已暂停" + else: + state = "Downloading" + dlspeed = StringUtils.str_filesize(torrent.rateDownload) + upspeed = StringUtils.str_filesize(torrent.rateUpload) + speed = "%s%sB/s %s%sB/s" % (chr(8595), + dlspeed, chr(8593), upspeed) + # 进度 + progress = round(torrent.progress, 1) + # 主键 + key = torrent.id + + torrent_info = {'id': key, 'speed': speed, + 'state': state, 'progress': progress} + if torrent_info not in DispTorrents: + DispTorrents.append(torrent_info) + return {"retcode": 0, "torrents": DispTorrents} + + def __del_unknown_path(self, data): + """ + 删除路径 + """ + tids = data.get("id") + if isinstance(tids, list): + for tid in tids: + if not tid: + continue + self.dbhelper.delete_transfer_unknown(tid) + return {"retcode": 0} + else: + retcode = self.dbhelper.delete_transfer_unknown(tids) + return {"retcode": retcode} + + def __rename(self, data): + """ + 手工转移 + """ + path = dest_dir = None + syncmod = ModuleConf.RMT_MODES.get(data.get("syncmod")) + logid = data.get("logid") + if logid: + paths = self.dbhelper.get_transfer_path_by_id(logid) + if paths: + path = os.path.join( + paths[0].SOURCE_PATH, paths[0].SOURCE_FILENAME) + dest_dir = paths[0].DEST + else: + return {"retcode": -1, "retmsg": "未查询到转移日志记录"} + else: + unknown_id = data.get("unknown_id") + if unknown_id: + paths = self.dbhelper.get_unknown_path_by_id(unknown_id) + if paths: + path = paths[0].PATH + dest_dir = paths[0].DEST + else: + return {"retcode": -1, "retmsg": "未查询到未识别记录"} + if not dest_dir: + dest_dir = "" + if not path: + return {"retcode": -1, "retmsg": "输入路径有误"} + tmdbid = data.get("tmdb") + mtype = data.get("type") + season = data.get("season") + episode_format = data.get("episode_format") + episode_details = data.get("episode_details") + episode_offset = data.get("episode_offset") + min_filesize = data.get("min_filesize") + if mtype in MovieTypes: + media_type = MediaType.MOVIE + elif mtype in TvTypes: + media_type = MediaType.TV + else: + media_type = MediaType.ANIME + # 如果改次手动修复时一个单文件,自动修复改目录下同名文件,需要配合episode_format生效 + need_fix_all = False + if os.path.splitext(path)[-1].lower() in RMT_MEDIAEXT and episode_format: + path = os.path.dirname(path) + need_fix_all = True + # 开始转移 + succ_flag, ret_msg = self.__manual_transfer(inpath=path, + syncmod=syncmod, + outpath=dest_dir, + media_type=media_type, + episode_format=episode_format, + episode_details=episode_details, + episode_offset=episode_offset, + need_fix_all=need_fix_all, + min_filesize=min_filesize, + tmdbid=tmdbid, + season=season) + if succ_flag: + if not need_fix_all and not logid: + # 更新记录状态 + self.dbhelper.update_transfer_unknown_state(path) + return {"retcode": 0, "retmsg": "转移成功"} + else: + return {"retcode": 2, "retmsg": ret_msg} + + def __rename_udf(self, data): + """ + 自定义识别 + """ + inpath = data.get("inpath") + if not os.path.exists(inpath): + return {"retcode": -1, "retmsg": "输入路径不存在"} + outpath = data.get("outpath") + syncmod = ModuleConf.RMT_MODES.get(data.get("syncmod")) + tmdbid = data.get("tmdb") + mtype = data.get("type") + season = data.get("season") + episode_format = data.get("episode_format") + episode_details = data.get("episode_details") + episode_offset = data.get("episode_offset") + min_filesize = data.get("min_filesize") + if mtype in MovieTypes: + media_type = MediaType.MOVIE + elif mtype in TvTypes: + media_type = MediaType.TV + else: + media_type = MediaType.ANIME + # 开始转移 + succ_flag, ret_msg = self.__manual_transfer(inpath=inpath, + syncmod=syncmod, + outpath=outpath, + media_type=media_type, + episode_format=episode_format, + episode_details=episode_details, + episode_offset=episode_offset, + min_filesize=min_filesize, + tmdbid=tmdbid, + season=season) + if succ_flag: + return {"retcode": 0, "retmsg": "转移成功"} + else: + return {"retcode": 2, "retmsg": ret_msg} + + @staticmethod + def __manual_transfer(inpath, + syncmod, + outpath=None, + media_type=None, + episode_format=None, + episode_details=None, + episode_offset=None, + min_filesize=None, + tmdbid=None, + season=None, + need_fix_all=False + ): + """ + 开始手工转移文件 + """ + inpath = os.path.normpath(inpath) + if outpath: + outpath = os.path.normpath(outpath) + if not os.path.exists(inpath): + return False, "输入路径不存在" + if tmdbid: + # 有输入TMDBID + tmdb_info = Media().get_tmdb_info(mtype=media_type, tmdbid=tmdbid) + if not tmdb_info: + return False, "识别失败,无法查询到TMDB信息" + # 按识别的信息转移 + succ_flag, ret_msg = FileTransfer().transfer_media(in_from=SyncType.MAN, + in_path=inpath, + rmt_mode=syncmod, + target_dir=outpath, + tmdb_info=tmdb_info, + media_type=media_type, + season=season, + episode=( + EpisodeFormat(episode_format, + episode_details, + episode_offset), + need_fix_all), + min_filesize=min_filesize, + udf_flag=True) + else: + # 按识别的信息转移 + succ_flag, ret_msg = FileTransfer().transfer_media(in_from=SyncType.MAN, + in_path=inpath, + rmt_mode=syncmod, + target_dir=outpath, + media_type=media_type, + episode=( + EpisodeFormat(episode_format, + episode_details, + episode_offset), + need_fix_all), + min_filesize=min_filesize, + udf_flag=True) + return succ_flag, ret_msg + + def __delete_history(self, data): + """ + 删除识别记录及文件 + """ + logids = data.get('logids') + flag = data.get('flag') + for logid in logids: + # 读取历史记录 + paths = self.dbhelper.get_transfer_path_by_id(logid) + if paths: + # 删除记录 + self.dbhelper.delete_transfer_log_by_id(logid) + # 根据flag删除文件 + source_path = paths[0].SOURCE_PATH + source_filename = paths[0].SOURCE_FILENAME + dest = paths[0].DEST + dest_path = paths[0].DEST_PATH + dest_filename = paths[0].DEST_FILENAME + if flag in ["del_source", "del_all"]: + del_flag, del_msg = self.delete_media_file( + source_path, source_filename) + if not del_flag: + log.error(f"【History】{del_msg}") + else: + log.info(f"【History】{del_msg}") + if flag in ["del_dest", "del_all"]: + if dest_path and dest_filename: + del_flag, del_msg = self.delete_media_file( + dest_path, dest_filename) + if not del_flag: + log.error(f"【History】{del_msg}") + else: + log.info(f"【History】{del_msg}") + else: + meta_info = MetaInfo(title=source_filename) + meta_info.title = paths[0].TITLE + meta_info.category = paths[0].CATEGORY + meta_info.year = paths[0].YEAR + if paths[0].SEASON_EPISODE: + meta_info.begin_season = int( + str(paths[0].SEASON_EPISODE).replace("S", "")) + if paths[0].TYPE == MediaType.MOVIE.value: + meta_info.type = MediaType.MOVIE + else: + meta_info.type = MediaType.TV + # 删除文件 + dest_path = FileTransfer().get_dest_path_by_info(dest=dest, meta_info=meta_info) + if dest_path and dest_path.find(meta_info.title) != -1: + rm_parent_dir = False + if not meta_info.get_season_list(): + # 电影,删除整个目录 + try: + shutil.rmtree(dest_path) + except Exception as e: + ExceptionUtils.exception_traceback(e) + elif not meta_info.get_episode_string(): + # 电视剧但没有集数,删除季目录 + try: + shutil.rmtree(dest_path) + except Exception as e: + ExceptionUtils.exception_traceback(e) + rm_parent_dir = True + else: + # 有集数的电视剧,删除对应的集数文件 + for dest_file in PathUtils.get_dir_files(dest_path): + file_meta_info = MetaInfo( + os.path.basename(dest_file)) + if file_meta_info.get_episode_list() and set( + file_meta_info.get_episode_list() + ).issubset(set(meta_info.get_episode_list())): + try: + os.remove(dest_file) + except Exception as e: + ExceptionUtils.exception_traceback( + e) + rm_parent_dir = True + if rm_parent_dir \ + and not PathUtils.get_dir_files(os.path.dirname(dest_path), exts=RMT_MEDIAEXT): + # 没有媒体文件时,删除整个目录 + try: + shutil.rmtree(os.path.dirname(dest_path)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"retcode": 0} + + @staticmethod + def delete_media_file(filedir, filename): + """ + 删除媒体文件,空目录也支被删除 + """ + filedir = os.path.normpath(filedir).replace("\\", "/") + file = os.path.join(filedir, filename) + try: + if not os.path.exists(file): + return False, f"{file} 不存在" + os.remove(file) + nfoname = f"{os.path.splitext(filename)[0]}.nfo" + nfofile = os.path.join(filedir, nfoname) + if os.path.exists(nfofile): + os.remove(nfofile) + # 检查空目录并删除 + if re.findall(r"^S\d{2}|^Season", os.path.basename(filedir), re.I): + # 当前是季文件夹,判断并删除 + seaon_dir = filedir + if seaon_dir.count('/') > 1 and not PathUtils.get_dir_files(seaon_dir, exts=RMT_MEDIAEXT): + shutil.rmtree(seaon_dir) + # 媒体文件夹 + media_dir = os.path.dirname(seaon_dir) + else: + media_dir = filedir + # 检查并删除媒体文件夹,非根目录且目录大于二级,且没有媒体文件时才会删除 + if media_dir != '/' \ + and media_dir.count('/') > 1 \ + and not re.search(r'[a-zA-Z]:/$', media_dir) \ + and not PathUtils.get_dir_files(media_dir, exts=RMT_MEDIAEXT): + shutil.rmtree(media_dir) + return True, f"{file} 删除成功" + except Exception as e: + ExceptionUtils.exception_traceback(e) + return True, f"{file} 删除失败" + + @staticmethod + def __logging(data): + """ + 查询实时日志 + """ + log_list = [] + refresh_new = data.get('refresh_new') + source = data.get('source') + + if not source: + if not refresh_new: + log_list = list(log.LOG_QUEUE) + elif log.LOG_INDEX: + if log.LOG_INDEX > len(list(log.LOG_QUEUE)): + log_list = list(log.LOG_QUEUE) + else: + log_list = list(log.LOG_QUEUE)[-log.LOG_INDEX:] + log.LOG_INDEX = 0 + else: + queue_logs = list(log.LOG_QUEUE) + for message in queue_logs: + if str(message.get("source")) == source: + log_list.append(message) + else: + continue + + if refresh_new: + if int(refresh_new) < len(log_list): + log_list = log_list[int(refresh_new):] + elif int(refresh_new) >= len(log_list): + log_list = [] + return {"loglist": log_list} + + @staticmethod + def __version(data): + """ + 检查新版本 + """ + version, url, flag = WebUtils.get_latest_version() + if flag: + return {"code": 0, "version": version, "url": url} + return {"code": -1, "version": "", "url": ""} + + def __update_site(self, data): + """ + 维护站点信息 + """ + + def __is_site_duplicate(query_name, query_tid): + # 检查是否重名 + _sites = self.dbhelper.get_site_by_name(name=query_name) + for site in _sites: + site_id = site.ID + if str(site_id) != str(query_tid): + return True + return False + + tid = data.get('site_id') + name = data.get('site_name') + site_pri = data.get('site_pri') + rssurl = data.get('site_rssurl') + signurl = data.get('site_signurl') + cookie = data.get('site_cookie') + note = data.get('site_note') + if isinstance(note, dict): + note = json.dumps(note) + rss_uses = data.get('site_include') + + if __is_site_duplicate(name, tid): + return {"code": 400, "msg": "站点名称重复"} + + if tid: + sites = self.dbhelper.get_site_by_id(tid) + # 站点不存在 + if not sites: + return {"code": 400, "msg": "站点不存在"} + + old_name = sites[0].NAME + + ret = self.dbhelper.update_config_site(tid=tid, + name=name, + site_pri=site_pri, + rssurl=rssurl, + signurl=signurl, + cookie=cookie, + note=note, + rss_uses=rss_uses) + if ret and (name != old_name): + # 更新历史站点数据信息 + self.dbhelper.update_site_user_statistics_site_name( + name, old_name) + self.dbhelper.update_site_seed_info_site_name(name, old_name) + self.dbhelper.update_site_statistics_site_name(name, old_name) + + else: + ret = self.dbhelper.insert_config_site(name=name, + site_pri=site_pri, + rssurl=rssurl, + signurl=signurl, + cookie=cookie, + note=note, + rss_uses=rss_uses) + # 生效站点配置 + Sites().init_config() + # 初始化刷流任务 + BrushTask().init_config() + return {"code": ret} + + @staticmethod + def __get_site(data): + """ + 查询单个站点信息 + """ + tid = data.get("id") + site_free = False + site_2xfree = False + site_hr = False + if tid: + ret = Sites().get_sites(siteid=tid) + if ret.get("rssurl"): + site_attr = Sites().get_grapsite_conf(ret.get("rssurl")) + if site_attr.get("FREE"): + site_free = True + if site_attr.get("2XFREE"): + site_2xfree = True + if site_attr.get("HR"): + site_hr = True + else: + ret = [] + return {"code": 0, "site": ret, "site_free": site_free, "site_2xfree": site_2xfree, "site_hr": site_hr} + + @staticmethod + def __get_sites(data): + """ + 查询多个站点信息 + """ + rss = True if data.get("rss") else False + brush = True if data.get("brush") else False + signin = True if data.get("signin") else False + statistic = True if data.get("statistic") else False + basic = True if data.get("basic") else False + if basic: + sites = Sites().get_site_dict(rss=rss, + brush=brush, + signin=signin, + statistic=statistic) + else: + sites = Sites().get_sites(rss=rss, + brush=brush, + signin=signin, + statistic=statistic) + return {"code": 0, "sites": sites} + + def __del_site(self, data): + """ + 删除单个站点信息 + """ + tid = data.get("id") + if tid: + ret = self.dbhelper.delete_config_site(tid) + Sites().init_config() + BrushTask().init_config() + return {"code": ret} + else: + return {"code": 0} + + def __restart(self, data): + """ + 重启 + """ + # 退出主进程 + self.restart_server() + return {"code": 0} + + def update_system(self, data=None): + """ + 更新 + """ + # 升级 + if SystemUtils.is_synology(): + if SystemUtils.execute('/bin/ps -w -x | grep -v grep | grep -w "nastool update" | wc -l') == '0': + # 调用群晖套件内置命令升级 + os.system('nastool update') + # 重启 + self.restart_server() + else: + # 清除git代理 + os.system("git config --global --unset http.proxy") + os.system("git config --global --unset https.proxy") + # 设置git代理 + proxy = Config().get_proxies() or {} + http_proxy = proxy.get("http") + https_proxy = proxy.get("https") + if http_proxy or https_proxy: + os.system( + f"git config --global http.proxy {http_proxy or https_proxy}") + os.system( + f"git config --global https.proxy {https_proxy or http_proxy}") + # 清理 + os.system("git clean -dffx") + # 升级 + branch = "dev" if os.environ.get( + "NASTOOL_VERSION") == "dev" else "master" + os.system(f"git fetch --depth 1 origin {branch}") + os.system(f"git reset --hard origin/{branch}") + os.system("git submodule update --init --recursive") + # 安装依赖 + os.system('pip install -r /nas-tools/requirements.txt') + # 重启 + self.restart_server() + return {"code": 0} + + def __reset_db_version(self, data): + """ + 重置数据库版本 + """ + try: + self.dbhelper.drop_table("alembic_version") + return {"code": 0} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + @staticmethod + def __logout(data): + """ + 注销 + """ + logout_user() + return {"code": 0} + + def __update_config(self, data): + """ + 更新配置信息 + """ + cfg = Config().get_config() + cfgs = dict(data).items() + # 仅测试不保存 + config_test = False + # 修改配置 + for key, value in cfgs: + if key == "test" and value: + config_test = True + continue + # 生效配置 + cfg = self.set_config_value(cfg, key, value) + + # 保存配置 + if not config_test: + Config().save_config(cfg) + + return {"code": 0} + + def __add_or_edit_sync_path(self, data): + """ + 维护同步目录 + """ + sid = data.get("sid") + source = data.get("from") + dest = data.get("to") + unknown = data.get("unknown") + mode = data.get("syncmod") + rename = 1 if StringUtils.to_bool(data.get("rename"), False) else 0 + enabled = 1 if StringUtils.to_bool(data.get("enabled"), False) else 0 + # 源目录检查 + if not source: + return {"code": 1, "msg": f'源目录不能为空'} + if not os.path.exists(source): + return {"code": 1, "msg": f'{source}目录不存在'} + # windows目录用\,linux目录用/ + source = os.path.normpath(source) + # 目的目录检查,目的目录可为空 + if dest: + dest = os.path.normpath(dest) + if PathUtils.is_path_in_path(source, dest): + return {"code": 1, "msg": "目的目录不可包含在源目录中"} + if unknown: + unknown = os.path.normpath(unknown) + + # 硬链接不能跨盘 + if mode == "link" and dest: + common_path = os.path.commonprefix([source, dest]) + if not common_path or common_path == "/": + return {"code": 1, "msg": "硬链接不能跨盘"} + + # 编辑先删再增 + if sid: + self.dbhelper.delete_config_sync_path(sid) + # 若启用,则关闭其他相同源目录的同步目录 + if enabled == 1: + self.dbhelper.check_config_sync_paths(source=source, + enabled=0) + # 插入数据库 + self.dbhelper.insert_config_sync_path(source=source, + dest=dest, + unknown=unknown, + mode=mode, + rename=rename, + enabled=enabled) + Sync().init_config() + return {"code": 0, "msg": ""} + + def __get_sync_path(self, data): + """ + 查询同步目录 + """ + try: + sid = data.get("sid") + sync_item = self.dbhelper.get_config_sync_paths(sid=sid)[0] + syncpath = {'id': sync_item.ID, + 'from': sync_item.SOURCE, + 'to': sync_item.DEST or "", + 'unknown': sync_item.UNKNOWN or "", + 'syncmod': sync_item.MODE, + 'rename': sync_item.RENAME, + 'enabled': sync_item.ENABLED} + return {"code": 0, "data": syncpath} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": "查询识别词失败"} + + def __delete_sync_path(self, data): + """ + 移出同步目录 + """ + sid = data.get("sid") + self.dbhelper.delete_config_sync_path(sid) + Sync().init_config() + return {"code": 0} + + def __check_sync_path(self, data): + """ + 维护同步目录 + """ + flag = data.get("flag") + sid = data.get("sid") + checked = data.get("checked") + if flag == "rename": + self.dbhelper.check_config_sync_paths(sid=sid, + rename=1 if checked else 0) + Sync().init_config() + return {"code": 0} + elif flag == "enable": + # 若启用,则关闭其他相同源目录的同步目录 + if checked: + sync_item = self.dbhelper.get_config_sync_paths(sid=sid)[0] + self.dbhelper.check_config_sync_paths(source=sync_item.SOURCE, + enabled=0) + self.dbhelper.check_config_sync_paths(sid=sid, + enabled=1 if checked else 0) + Sync().init_config() + return {"code": 0} + else: + return {"code": 1} + + def __remove_rss_media(self, data): + """ + 移除RSS订阅 + """ + name = data.get("name") + mtype = data.get("type") + year = data.get("year") + season = data.get("season") + rssid = data.get("rssid") + page = data.get("page") + tmdbid = data.get("tmdbid") + if not str(tmdbid).isdigit(): + tmdbid = None + if name: + name = MetaInfo(title=name).get_name() + if mtype: + if mtype in MovieTypes: + self.dbhelper.delete_rss_movie( + title=name, year=year, rssid=rssid, tmdbid=tmdbid) + else: + self.dbhelper.delete_rss_tv( + title=name, season=season, rssid=rssid, tmdbid=tmdbid) + return {"code": 0, "page": page, "name": name} + + def __add_rss_media(self, data): + """ + 添加RSS订阅 + """ + name = data.get("name") + _subscribe = Subscribe() + year = data.get("year") + keyword = data.get("keyword") + season = data.get("season") + fuzzy_match = data.get("fuzzy_match") + mediaid = data.get("mediaid") + rss_sites = data.get("rss_sites") + search_sites = data.get("search_sites") + over_edition = data.get("over_edition") + filter_restype = data.get("filter_restype") + filter_pix = data.get("filter_pix") + filter_team = data.get("filter_team") + filter_rule = data.get("filter_rule") + save_path = data.get("save_path") + download_setting = data.get("download_setting") + total_ep = data.get("total_ep") + current_ep = data.get("current_ep") + rssid = data.get("rssid") + page = data.get("page") + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + media_info = None + if isinstance(season, list): + code = 0 + msg = "" + for sea in season: + code, msg, media_info = _subscribe.add_rss_subscribe(mtype=mtype, + name=name, + year=year, + keyword=keyword, + season=sea, + fuzzy_match=fuzzy_match, + mediaid=mediaid, + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + rssid=rssid) + if code != 0: + break + else: + code, msg, media_info = _subscribe.add_rss_subscribe(mtype=mtype, + name=name, + year=year, + keyword=keyword, + season=season, + fuzzy_match=fuzzy_match, + mediaid=mediaid, + rss_sites=rss_sites, + search_sites=search_sites, + over_edition=over_edition, + filter_restype=filter_restype, + filter_pix=filter_pix, + filter_team=filter_team, + filter_rule=filter_rule, + save_path=save_path, + download_setting=download_setting, + total_ep=total_ep, + current_ep=current_ep, + rssid=rssid) + if not rssid and media_info: + if mtype == MediaType.MOVIE: + rssid = self.dbhelper.get_rss_movie_id( + title=name, tmdbid=media_info.tmdb_id) + else: + rssid = self.dbhelper.get_rss_tv_id( + title=name, tmdbid=media_info.tmdb_id) + return {"code": code, "msg": msg, "page": page, "name": name, "rssid": rssid} + + def re_identification(self, data): + """ + 未识别的重新识别 + """ + flag = data.get("flag") + ids = data.get("ids") + ret_flag = True + ret_msg = [] + if flag == "unidentification": + for wid in ids: + paths = self.dbhelper.get_unknown_path_by_id(wid) + if paths: + path = paths[0].PATH + dest_dir = paths[0].DEST + rmt_mode = ModuleConf.get_enum_item( + RmtMode, paths[0].MODE) if paths[0].MODE else None + else: + return {"retcode": -1, "retmsg": "未查询到未识别记录"} + if not dest_dir: + dest_dir = "" + if not path: + return {"retcode": -1, "retmsg": "未识别路径有误"} + succ_flag, msg = FileTransfer().transfer_media(in_from=SyncType.MAN, + rmt_mode=rmt_mode, + in_path=path, + target_dir=dest_dir) + if succ_flag: + self.dbhelper.update_transfer_unknown_state(path) + else: + ret_flag = False + if msg not in ret_msg: + ret_msg.append(msg) + elif flag == "history": + for wid in ids: + paths = self.dbhelper.get_transfer_path_by_id(wid) + if paths: + path = os.path.join( + paths[0].SOURCE_PATH, paths[0].SOURCE_FILENAME) + dest_dir = paths[0].DEST + rmt_mode = ModuleConf.get_enum_item( + RmtMode, paths[0].MODE) if paths[0].MODE else None + else: + return {"retcode": -1, "retmsg": "未查询到转移日志记录"} + if not dest_dir: + dest_dir = "" + if not path: + return {"retcode": -1, "retmsg": "未识别路径有误"} + succ_flag, msg = FileTransfer().transfer_media(in_from=SyncType.MAN, + rmt_mode=rmt_mode, + in_path=path, + target_dir=dest_dir) + if not succ_flag: + ret_flag = False + if msg not in ret_msg: + ret_msg.append(msg) + if ret_flag: + return {"retcode": 0, "retmsg": "转移成功"} + else: + return {"retcode": 2, "retmsg": "、".join(ret_msg)} + + def __media_info(self, data): + """ + 查询媒体信息 + """ + mediaid = data.get("id") + mtype = data.get("type") + title = data.get("title") + year = data.get("year") + page = data.get("page") + rssid = data.get("rssid") + seasons = [] + link_url = "" + vote_average = 0 + poster_path = "" + release_date = "" + overview = "" + # 类型 + if mtype in MovieTypes: + media_type = MediaType.MOVIE + else: + media_type = MediaType.TV + + # 先取订阅信息 + rssid_ok = False + if rssid: + rssid = str(rssid) + if media_type == MediaType.MOVIE: + rssinfo = Subscribe().get_subscribe_movies(rid=rssid) + else: + rssinfo = Subscribe().get_subscribe_tvs(rid=rssid) + if not rssinfo: + return { + "code": 1, + "retmsg": "无法查询到订阅信息", + "rssid": rssid, + "type_str": media_type.value + } + overview = rssinfo[rssid].get("overview") + poster_path = rssinfo[rssid].get("poster") + title = rssinfo[rssid].get("name") + vote_average = rssinfo[rssid].get("vote") + year = rssinfo[rssid].get("year") + release_date = rssinfo[rssid].get("release_date") + link_url = Media().get_detail_url(mtype=media_type, + tmdbid=rssinfo[rssid].get("tmdbid")) + if overview and poster_path: + rssid_ok = True + + # 订阅信息不足 + if not rssid_ok: + if mediaid: + media = WebUtils.get_mediainfo_from_id( + mtype=media_type, mediaid=mediaid) + else: + media = Media().get_media_info( + title=f"{title} {year}", mtype=media_type) + if not media or not media.tmdb_info: + return { + "code": 1, + "retmsg": "无法查询到TMDB信息", + "rssid": rssid, + "type_str": media_type.value + } + if not mediaid: + mediaid = media.tmdb_id + link_url = media.get_detail_url() + overview = media.overview + poster_path = media.get_poster_image() + title = media.title + vote_average = round(float(media.vote_average or 0), 1) + year = media.year + if media_type != MediaType.MOVIE: + release_date = media.tmdb_info.get('first_air_date') + seasons = [{ + "text": "第%s季" % cn2an.an2cn(season.get("season_number"), mode='low'), + "num": season.get("season_number")} for season in + Media().get_tmdb_tv_seasons(tv_info=media.tmdb_info)] + else: + release_date = media.tmdb_info.get('release_date') + + # 查订阅信息 + if not rssid: + if media_type == MediaType.MOVIE: + rssid = self.dbhelper.get_rss_movie_id( + title=title, tmdbid=mediaid) + else: + rssid = self.dbhelper.get_rss_tv_id( + title=title, tmdbid=mediaid) + + return { + "code": 0, + "type": mtype, + "type_str": media_type.value, + "page": page, + "title": title, + "vote_average": vote_average, + "poster_path": poster_path, + "release_date": release_date, + "year": year, + "overview": overview, + "link_url": link_url, + "tmdbid": mediaid, + "rssid": rssid, + "seasons": seasons + } + + @staticmethod + def __test_connection(data): + """ + 测试连通性 + """ + # 支持两种传入方式:命令数组或单个命令,单个命令时xx|xx模式解析为模块和类,进行动态引入 + command = data.get("command") + ret = None + if command: + try: + module_obj = None + if isinstance(command, list): + for cmd_str in command: + ret = eval(cmd_str) + if not ret: + break + else: + if command.find("|") != -1: + module = command.split("|")[0] + class_name = command.split("|")[1] + module_obj = getattr( + importlib.import_module(module), class_name)() + if hasattr(module_obj, "init_config"): + module_obj.init_config() + ret = module_obj.get_status() + else: + ret = eval(command) + # 重载配置 + Config().init_config() + if module_obj: + if hasattr(module_obj, "init_config"): + module_obj.init_config() + except Exception as e: + ret = None + ExceptionUtils.exception_traceback(e) + return {"code": 0 if ret else 1} + return {"code": 0} + + def __user_manager(self, data): + """ + 用户管理 + """ + oper = data.get("oper") + name = data.get("name") + if oper == "add": + password = generate_password_hash(str(data.get("password"))) + pris = data.get("pris") + if isinstance(pris, list): + pris = ",".join(pris) + ret = self.dbhelper.insert_user(name, password, pris) + else: + ret = self.dbhelper.delete_user(name) + + if ret == 1 or ret: + return {"code": 0, "success": False} + return {"code": -1, "success": False, 'message': '操作失败'} + + @staticmethod + def __refresh_rss(data): + """ + 重新搜索RSS + """ + mtype = data.get("type") + rssid = data.get("rssid") + page = data.get("page") + if mtype == "MOV": + ThreadHelper().start_thread(Subscribe().subscribe_search_movie, (rssid,)) + else: + ThreadHelper().start_thread(Subscribe().subscribe_search_tv, (rssid,)) + return {"code": 0, "page": page} + + @staticmethod + def get_system_message(lst_time): + messages = MessageCenter().get_system_messages(lst_time=lst_time) + if messages: + lst_time = messages[0].get("time") + return { + "code": 0, + "message": messages, + "lst_time": lst_time + } + + def __refresh_message(self, data): + """ + 刷新首页消息中心 + """ + lst_time = data.get("lst_time") + system_msg = self.get_system_message(lst_time=lst_time) + messages = system_msg.get("message") + lst_time = system_msg.get("lst_time") + message_html = [] + for message in list(reversed(messages)): + level = "bg-red" if message.get("level") == "ERROR" else "" + content = re.sub(r"#+", "
", + re.sub(r"<[^>]+>", "", + re.sub(r"
", "####", message.get("content"), flags=re.IGNORECASE))) + message_html.append(f""" +
+
+
+ +
+
+ {message.get("title")} +
{content}
+
{message.get("time")}
+
+
+
+ """) + return {"code": 0, "message": message_html, "lst_time": lst_time} + + @staticmethod + def __delete_tmdb_cache(data): + """ + 删除tmdb缓存 + """ + if MetaHelper().delete_meta_data(data.get("cache_key")): + MetaHelper().save_meta_data() + return {"code": 0} + + @staticmethod + def __movie_calendar_data(data): + """ + 查询电影上映日期 + """ + tid = data.get("id") + rssid = data.get("rssid") + if tid and tid.startswith("DB:"): + doubanid = tid.replace("DB:", "") + douban_info = DouBan().get_douban_detail( + doubanid=doubanid, mtype=MediaType.MOVIE) + if not douban_info: + return {"code": 1, "retmsg": "无法查询到豆瓣信息"} + poster_path = douban_info.get("cover_url") or "" + title = douban_info.get("title") + rating = douban_info.get("rating", {}) or {} + vote_average = rating.get("value") or "无" + release_date = douban_info.get("pubdate") + if release_date: + release_date = re.sub( + r"\(.*\)", "", douban_info.get("pubdate")[0]) + if not release_date: + return {"code": 1, "retmsg": "上映日期不正确"} + else: + return {"code": 0, + "type": "电影", + "title": title, + "start": release_date, + "id": tid, + "year": release_date[0:4] if release_date else "", + "poster": poster_path, + "vote_average": vote_average, + "rssid": rssid + } + else: + if tid: + tmdb_info = Media().get_tmdb_info(mtype=MediaType.MOVIE, tmdbid=tid) + else: + return {"code": 1, "retmsg": "没有TMDBID信息"} + if not tmdb_info: + return {"code": 1, "retmsg": "无法查询到TMDB信息"} + poster_path = TMDB_IMAGE_W500_URL % tmdb_info.get('poster_path') if tmdb_info.get( + 'poster_path') else "" + title = tmdb_info.get('title') + vote_average = tmdb_info.get("vote_average") + release_date = tmdb_info.get('release_date') + if not release_date: + return {"code": 1, "retmsg": "上映日期不正确"} + else: + return {"code": 0, + "type": "电影", + "title": title, + "start": release_date, + "id": tid, + "year": release_date[0:4] if release_date else "", + "poster": poster_path, + "vote_average": vote_average, + "rssid": rssid + } + + @staticmethod + def __tv_calendar_data(data): + """ + 查询电视剧上映日期 + """ + tid = data.get("id") + season = data.get("season") + name = data.get("name") + rssid = data.get("rssid") + if tid and tid.startswith("DB:"): + doubanid = tid.replace("DB:", "") + douban_info = DouBan().get_douban_detail(doubanid=doubanid, mtype=MediaType.TV) + if not douban_info: + return {"code": 1, "retmsg": "无法查询到豆瓣信息"} + poster_path = douban_info.get("cover_url") or "" + title = douban_info.get("title") + rating = douban_info.get("rating", {}) or {} + vote_average = rating.get("value") or "无" + release_date = re.sub(r"\(.*\)", "", douban_info.get("pubdate")[0]) + if not release_date: + return {"code": 1, "retmsg": "上映日期不正确"} + else: + return {"code": 0, + "type": "电视剧", + "title": title, + "start": release_date, + "id": tid, + "year": release_date[0:4] if release_date else "", + "poster": poster_path, + "vote_average": vote_average, + "rssid": rssid + } + else: + if tid: + tmdb_info = Media().get_tmdb_tv_season_detail(tmdbid=tid, season=season) + else: + return {"code": 1, "retmsg": "没有TMDBID信息"} + if not tmdb_info: + return {"code": 1, "retmsg": "无法查询到TMDB信息"} + episode_events = [] + air_date = tmdb_info.get("air_date") + if not tmdb_info.get("poster_path"): + tv_tmdb_info = Media().get_tmdb_info(mtype=MediaType.TV, tmdbid=tid) + if tv_tmdb_info: + poster_path = TMDB_IMAGE_W500_URL % tv_tmdb_info.get( + "poster_path") + else: + poster_path = "" + else: + poster_path = TMDB_IMAGE_W500_URL % tmdb_info.get( + "poster_path") + year = air_date[0:4] if air_date else "" + for episode in tmdb_info.get("episodes"): + episode_events.append({ + "type": "剧集", + "title": "%s 第%s季第%s集" % ( + name, + season, + episode.get("episode_number") + ) if season != 1 else "%s 第%s集" % ( + name, + episode.get("episode_number") + ), + "start": episode.get("air_date"), + "id": tid, + "year": year, + "poster": poster_path, + "vote_average": episode.get("vote_average") or "无", + "rssid": rssid + }) + return {"code": 0, "events": episode_events} + + @staticmethod + def __rss_detail(data): + rid = data.get("rssid") + mtype = data.get("rsstype") + if mtype in MovieTypes: + rssdetail = Subscribe().get_subscribe_movies(rid=rid) + if not rssdetail: + return {"code": 1} + rssdetail = list(rssdetail.values())[0] + rssdetail["type"] = "MOV" + else: + rssdetail = Subscribe().get_subscribe_tvs(rid=rid) + if not rssdetail: + return {"code": 1} + rssdetail = list(rssdetail.values())[0] + rssdetail["type"] = "TV" + return {"code": 0, "detail": rssdetail} + + @staticmethod + def __modify_tmdb_cache(data): + """ + 修改TMDB缓存的标题 + """ + if MetaHelper().modify_meta_data(data.get("key"), data.get("title")): + MetaHelper().save_meta_data(force=True) + return {"code": 0} + + def truncate_blacklist(self, data): + """ + 清空文件转移黑名单记录 + """ + self.dbhelper.truncate_transfer_blacklist() + return {"code": 0} + + def truncate_rsshistory(self, data): + """ + 清空RSS历史记录 + """ + self.dbhelper.truncate_rss_history() + self.dbhelper.truncate_rss_episodes() + return {"code": 0} + + def __add_brushtask(self, data): + """ + 新增刷流任务 + """ + # 输入值 + brushtask_id = data.get("brushtask_id") + brushtask_name = data.get("brushtask_name") + brushtask_site = data.get("brushtask_site") + brushtask_interval = data.get("brushtask_interval") + brushtask_downloader = data.get("brushtask_downloader") + brushtask_totalsize = data.get("brushtask_totalsize") + brushtask_state = data.get("brushtask_state") + brushtask_transfer = 'Y' if data.get("brushtask_transfer") else 'N' + brushtask_sendmessage = 'Y' if data.get( + "brushtask_sendmessage") else 'N' + brushtask_forceupload = 'Y' if data.get( + "brushtask_forceupload") else 'N' + brushtask_free = data.get("brushtask_free") + brushtask_hr = data.get("brushtask_hr") + brushtask_torrent_size = data.get("brushtask_torrent_size") + brushtask_include = data.get("brushtask_include") + brushtask_exclude = data.get("brushtask_exclude") + brushtask_dlcount = data.get("brushtask_dlcount") + brushtask_peercount = data.get("brushtask_peercount") + brushtask_seedtime = data.get("brushtask_seedtime") + brushtask_seedratio = data.get("brushtask_seedratio") + brushtask_seedsize = data.get("brushtask_seedsize") + brushtask_dltime = data.get("brushtask_dltime") + brushtask_avg_upspeed = data.get("brushtask_avg_upspeed") + brushtask_iatime = data.get("brushtask_iatime") + brushtask_pubdate = data.get("brushtask_pubdate") + brushtask_upspeed = data.get("brushtask_upspeed") + brushtask_downspeed = data.get("brushtask_downspeed") + # 选种规则 + rss_rule = { + "free": brushtask_free, + "hr": brushtask_hr, + "size": brushtask_torrent_size, + "include": brushtask_include, + "exclude": brushtask_exclude, + "dlcount": brushtask_dlcount, + "peercount": brushtask_peercount, + "pubdate": brushtask_pubdate, + "upspeed": brushtask_upspeed, + "downspeed": brushtask_downspeed + } + # 删除规则 + remove_rule = { + "time": brushtask_seedtime, + "ratio": brushtask_seedratio, + "uploadsize": brushtask_seedsize, + "dltime": brushtask_dltime, + "avg_upspeed": brushtask_avg_upspeed, + "iatime": brushtask_iatime + } + # 添加记录 + item = { + "name": brushtask_name, + "site": brushtask_site, + "free": brushtask_free, + "interval": brushtask_interval, + "downloader": brushtask_downloader, + "seed_size": brushtask_totalsize, + "transfer": brushtask_transfer, + "state": brushtask_state, + "rss_rule": rss_rule, + "remove_rule": remove_rule, + "sendmessage": brushtask_sendmessage, + "forceupload": brushtask_forceupload + } + self.dbhelper.insert_brushtask(brushtask_id, item) + + # 重新初始化任务 + BrushTask().init_config() + return {"code": 0} + + def __del_brushtask(self, data): + """ + 删除刷流任务 + """ + brush_id = data.get("id") + if brush_id: + self.dbhelper.delete_brushtask(brush_id) + # 重新初始化任务 + BrushTask().init_config() + return {"code": 0} + return {"code": 1} + + def __brushtask_detail(self, data): + """ + 查询刷流任务详情 + """ + brush_id = data.get("id") + brushtask = self.dbhelper.get_brushtasks(brush_id) + if not brushtask: + return {"code": 1, "task": {}} + site_info = Sites().get_sites(siteid=brushtask.SITE) + task = { + "id": brushtask.ID, + "name": brushtask.NAME, + "site": brushtask.SITE, + "interval": brushtask.INTEVAL, + "state": brushtask.STATE, + "downloader": brushtask.DOWNLOADER, + "transfer": brushtask.TRANSFER, + "free": brushtask.FREELEECH, + "rss_rule": eval(brushtask.RSS_RULE), + "remove_rule": eval(brushtask.REMOVE_RULE), + "seed_size": brushtask.SEED_SIZE, + "download_count": brushtask.DOWNLOAD_COUNT, + "remove_count": brushtask.REMOVE_COUNT, + "download_size": StringUtils.str_filesize(brushtask.DOWNLOAD_SIZE), + "upload_size": StringUtils.str_filesize(brushtask.UPLOAD_SIZE), + "lst_mod_date": brushtask.LST_MOD_DATE, + "site_url": StringUtils.get_base_url(site_info.get("signurl") or site_info.get("rssurl")), + "sendmessage": brushtask.SENDMESSAGE, + "forceupload": brushtask.FORCEUPLOAD + } + return {"code": 0, "task": task} + + def __add_downloader(self, data): + """ + 添加自定义下载器 + """ + test = data.get("test") + dl_id = data.get("id") + dl_name = data.get("name") + dl_type = data.get("type") + if test: + # 测试 + if dl_type == "qbittorrent": + downloader = Qbittorrent( + config={ + "qbhost": data.get("host"), + "qbport": data.get("port"), + "qbusername": data.get("username"), + "qbpassword": data.get("password") + }) + else: + downloader = Transmission( + config={ + "trhost": data.get("host"), + "trport": data.get("port"), + "trusername": data.get("username"), + "trpassword": data.get("password") + }) + if downloader.get_status(): + return {"code": 0} + else: + return {"code": 1} + else: + # 保存 + self.dbhelper.update_user_downloader( + did=dl_id, + name=dl_name, + dtype=dl_type, + user_config={ + "host": data.get("host"), + "port": data.get("port"), + "username": data.get("username"), + "password": data.get("password"), + "save_dir": data.get("save_dir") + }, + note=None) + BrushTask().init_config() + return {"code": 0} + + def __delete_downloader(self, data): + """ + 删除自定义下载器 + """ + dl_id = data.get("id") + if dl_id: + self.dbhelper.delete_user_downloader(dl_id) + BrushTask().init_config() + return {"code": 0} + + def __get_downloader(self, data): + """ + 查询自定义下载器 + """ + dl_id = data.get("id") + if dl_id: + info = self.dbhelper.get_user_downloaders(dl_id) + if info: + return {"code": 0, "info": info.as_dict()} + return {"code": 1} + + def __name_test(self, data): + """ + 名称识别测试 + """ + name = data.get("name") + if not name: + return {"code": -1} + media_info = Media().get_media_info(title=name) + if not media_info: + return {"code": 0, "data": {"name": "无法识别"}} + return {"code": 0, "data": self.mediainfo_dict(media_info)} + + @staticmethod + def mediainfo_dict(media_info): + if not media_info: + return {} + tmdb_id = media_info.tmdb_id + tmdb_link = media_info.get_detail_url() + tmdb_S_E_link = "" + if tmdb_id: + if media_info.get_season_string(): + tmdb_S_E_link = "%s/season/%s" % (tmdb_link, + media_info.get_season_seq()) + if media_info.get_episode_string(): + tmdb_S_E_link = "%s/episode/%s" % ( + tmdb_S_E_link, media_info.get_episode_seq()) + return { + "type": media_info.type.value if media_info.type else "", + "name": media_info.get_name(), + "title": media_info.title, + "year": media_info.year, + "season_episode": media_info.get_season_episode_string(), + "part": media_info.part, + "tmdbid": tmdb_id, + "tmdblink": tmdb_link, + "tmdb_S_E_link": tmdb_S_E_link, + "category": media_info.category, + "restype": media_info.resource_type, + "effect": media_info.resource_effect, + "pix": media_info.resource_pix, + "team": media_info.resource_team, + "video_codec": media_info.video_encode, + "audio_codec": media_info.audio_encode, + "org_string": media_info.org_string, + "ignored_words": media_info.ignored_words, + "replaced_words": media_info.replaced_words, + "offset_words": media_info.offset_words + } + + @staticmethod + def __rule_test(data): + title = data.get("title") + subtitle = data.get("subtitle") + size = data.get("size") + rulegroup = data.get("rulegroup") + if not title: + return {"code": -1} + meta_info = MetaInfo(title=title, subtitle=subtitle) + meta_info.size = float(size) * 1024 ** 3 if size else 0 + match_flag, res_order, match_msg = \ + Filter().check_torrent_filter(meta_info=meta_info, + filter_args={"rule": rulegroup}) + return { + "code": 0, + "flag": match_flag, + "text": "匹配" if match_flag else "未匹配", + "order": 100 - res_order if res_order else 0 + } + + @staticmethod + def __net_test(data): + target = data + if target == "image.tmdb.org": + target = target + "/t/p/w500/wwemzKWzjKYJFfCeiB57q3r4Bcm.png" + if target == "qyapi.weixin.qq.com": + target = target + "/cgi-bin/message/send" + target = "https://" + target + start_time = datetime.datetime.now() + if target.find("themoviedb") != -1 \ + or target.find("telegram") != -1 \ + or target.find("fanart") != -1 \ + or target.find("tmdb") != -1: + res = RequestUtils(proxies=Config().get_proxies(), + timeout=5).get_res(target) + else: + res = RequestUtils(timeout=5).get_res(target) + seconds = int((datetime.datetime.now() - + start_time).microseconds / 1000) + if not res: + return {"res": False, "time": "%s 毫秒" % seconds} + elif res.ok: + return {"res": True, "time": "%s 毫秒" % seconds} + else: + return {"res": False, "time": "%s 毫秒" % seconds} + + @staticmethod + def __get_site_activity(data): + """ + 查询site活动[上传,下载,魔力值] + :param data: {"name":site_name} + :return: + """ + if not data or "name" not in data: + return {"code": 1, "msg": "查询参数错误"} + + resp = {"code": 0} + + resp.update( + {"dataset": SiteUserInfo().get_pt_site_activity_history(data["name"])}) + return resp + + @staticmethod + def __get_site_history(data): + """ + 查询site 历史[上传,下载] + :param data: {"days":累计时间} + :return: + """ + if not data or "days" not in data or not isinstance(data["days"], int): + return {"code": 1, "msg": "查询参数错误"} + + resp = {"code": 0} + _, _, site, upload, download = SiteUserInfo().get_pt_site_statistics_history(data["days"] + 1) + + # 调整为dataset组织数据 + dataset = [["site", "upload", "download"]] + dataset.extend([[site, upload, download] + for site, upload, download in zip(site, upload, download)]) + resp.update({"dataset": dataset}) + return resp + + @staticmethod + def __get_site_seeding_info(data): + """ + 查询site 做种分布信息 大小,做种数 + :param data: {"name":site_name} + :return: + """ + if not data or "name" not in data: + return {"code": 1, "msg": "查询参数错误"} + + resp = {"code": 0} + + seeding_info = SiteUserInfo().get_pt_site_seeding_info( + data["name"]).get("seeding_info", []) + # 调整为dataset组织数据 + dataset = [["seeders", "size"]] + dataset.extend(seeding_info) + + resp.update({"dataset": dataset}) + return resp + + def __add_filtergroup(self, data): + """ + 新增规则组 + """ + name = data.get("name") + default = data.get("default") + if not name: + return {"code": -1} + self.dbhelper.add_filter_group(name, default) + Filter().init_config() + return {"code": 0} + + def __restore_filtergroup(self, data): + """ + 恢复初始规则组 + """ + groupids = data.get("groupids") + init_rulegroups = data.get("init_rulegroups") + for groupid in groupids: + try: + self.dbhelper.delete_filtergroup(groupid) + except Exception as err: + ExceptionUtils.exception_traceback(err) + for init_rulegroup in init_rulegroups: + if str(init_rulegroup.get("id")) == groupid: + for sql in init_rulegroup.get("sql"): + self.dbhelper.excute(sql) + Filter().init_config() + return {"code": 0} + + def __set_default_filtergroup(self, data): + groupid = data.get("id") + if not groupid: + return {"code": -1} + self.dbhelper.set_default_filtergroup(groupid) + Filter().init_config() + return {"code": 0} + + def __del_filtergroup(self, data): + groupid = data.get("id") + self.dbhelper.delete_filtergroup(groupid) + Filter().init_config() + return {"code": 0} + + def __add_filterrule(self, data): + rule_id = data.get("rule_id") + item = { + "group": data.get("group_id"), + "name": data.get("rule_name"), + "pri": data.get("rule_pri"), + "include": data.get("rule_include"), + "exclude": data.get("rule_exclude"), + "size": data.get("rule_sizelimit"), + "free": data.get("rule_free") + } + self.dbhelper.insert_filter_rule(ruleid=rule_id, item=item) + Filter().init_config() + return {"code": 0} + + def __del_filterrule(self, data): + ruleid = data.get("id") + self.dbhelper.delete_filterrule(ruleid) + Filter().init_config() + return {"code": 0} + + @staticmethod + def __filterrule_detail(data): + rid = data.get("ruleid") + groupid = data.get("groupid") + ruleinfo = Filter().get_rules(groupid=groupid, ruleid=rid) + if ruleinfo: + ruleinfo['include'] = "\n".join(ruleinfo.get("include")) + ruleinfo['exclude'] = "\n".join(ruleinfo.get("exclude")) + return {"code": 0, "info": ruleinfo} + + def get_recommend(self, data): + Type = data.get("type") + SubType = data.get("subtype") + CurrentPage = data.get("page") + if not CurrentPage: + CurrentPage = 1 + else: + CurrentPage = int(CurrentPage) + + res_list = [] + if Type in ['MOV', 'TV']: + if SubType == "hm": + # TMDB热门电影 + res_list = Media().get_tmdb_hot_movies(CurrentPage) + elif SubType == "ht": + # TMDB热门电视剧 + res_list = Media().get_tmdb_hot_tvs(CurrentPage) + elif SubType == "nm": + # TMDB最新电影 + res_list = Media().get_tmdb_new_movies(CurrentPage) + elif SubType == "nt": + # TMDB最新电视剧 + res_list = Media().get_tmdb_new_tvs(CurrentPage) + elif SubType == "dbom": + # 豆瓣正在上映 + res_list = DouBan().get_douban_online_movie(CurrentPage) + elif SubType == "dbhm": + # 豆瓣热门电影 + res_list = DouBan().get_douban_hot_movie(CurrentPage) + elif SubType == "dbht": + # 豆瓣热门电视剧 + res_list = DouBan().get_douban_hot_tv(CurrentPage) + elif SubType == "dbdh": + # 豆瓣热门动画 + res_list = DouBan().get_douban_hot_anime(CurrentPage) + elif SubType == "dbnm": + # 豆瓣最新电影 + res_list = DouBan().get_douban_new_movie(CurrentPage) + elif SubType == "dbtop": + # 豆瓣TOP250电影 + res_list = DouBan().get_douban_top250_movie(CurrentPage) + elif SubType == "dbzy": + # 豆瓣最新电视剧 + res_list = DouBan().get_douban_hot_show(CurrentPage) + elif SubType == "dbct": + # 华语口碑剧集榜 + res_list = DouBan().get_douban_chinese_weekly_tv(CurrentPage) + elif SubType == "dbgt": + # 全球口碑剧集榜 + res_list = DouBan().get_douban_weekly_tv_global(CurrentPage) + elif SubType == "sim": + # 相似推荐 + TmdbId = data.get("tmdbid") + res_list = self.__media_similar({ + "tmdbid": TmdbId, + "page": CurrentPage, + "type": Type + }).get("data") + elif SubType == "more": + # 更多推荐 + TmdbId = data.get("tmdbid") + res_list = self.__media_recommendations({ + "tmdbid": TmdbId, + "page": CurrentPage, + "type": Type + }).get("data") + elif SubType == "person": + # 人物作品 + PersonId = data.get("personid") + res_list = self.__person_medias({ + "personid": PersonId, + "type": Type, + "page": CurrentPage + }).get("data") + elif SubType == "bangumi": + # Bangumi每日放送 + Week = data.get("week") + res_list = Bangumi().get_bangumi_calendar(page=CurrentPage, week=Week) + elif Type == "SEARCH": + # 搜索词条 + Keyword = data.get("keyword") + Source = data.get("source") + medias = WebUtils.search_media_infos( + keyword=Keyword, source=Source, page=CurrentPage) + res_list = [media.to_dict() for media in medias] + elif Type == "DOWNLOADED": + # 近期下载 + res_list = self.get_downloaded({ + "page": CurrentPage + }).get("Items") + elif Type == "TRENDING": + # TMDB流行趋势 + res_list = Media().get_tmdb_trending_all_week(page=CurrentPage) + elif Type == "DISCOVER": + # TMDB发现 + mtype = MediaType.MOVIE if SubType in MovieTypes else MediaType.TV + # 过滤参数 with_genres with_original_language + params = data.get("params") or {} + res_list = Media().get_tmdb_discover(mtype=mtype, page=CurrentPage, params=params) + elif Type == "DOUBANTAG": + # 豆瓣发现 + mtype = MediaType.MOVIE if SubType in MovieTypes else MediaType.TV + # 参数 + params = data.get("params") or {} + # 排序 + sort = params.get("sort") or "T" + # 选中的分类 + tags = params.get("tags") or "" + # 过滤参数 + res_list = DouBan().get_douban_disover(mtype=mtype, + sort=sort, + tags=tags, + page=CurrentPage) + + # 补充存在与订阅状态 + filetransfer = FileTransfer() + for res in res_list: + fav, rssid = filetransfer.get_media_exists_flag(mtype=Type, + title=res.get( + "title"), + year=res.get( + "year"), + mediaid=res.get("id")) + res.update({ + 'fav': fav, + 'rssid': rssid + }) + return {"code": 0, "Items": res_list} + + def get_downloaded(self, data): + page = data.get("page") + Items = self.dbhelper.get_download_history(page=page) + if Items: + return {"code": 0, "Items": [{ + 'id': item.TMDBID, + 'orgid': item.TMDBID, + 'tmdbid': item.TMDBID, + 'title': item.TITLE, + 'type': 'MOV' if item.TYPE == "电影" else "TV", + 'media_type': item.TYPE, + 'year': item.YEAR, + 'vote': item.VOTE, + 'image': item.POSTER, + 'overview': item.TORRENT, + "date": item.DATE, + "site": item.SITE + } for item in Items]} + else: + return {"code": 0, "Items": []} + + @staticmethod + def parse_brush_rule_string(rules: dict): + if not rules: + return "" + rule_filter_string = {"gt": ">", "lt": "<", "bw": ""} + rule_htmls = [] + if rules.get("size"): + sizes = rules.get("size").split("#") + if sizes[0]: + if sizes[1]: + sizes[1] = sizes[1].replace(",", "-") + rule_htmls.append( + '种子大小: %s %sGB' + % (rule_filter_string.get(sizes[0]), sizes[1])) + if rules.get("pubdate"): + pubdates = rules.get("pubdate").split("#") + if pubdates[0]: + if pubdates[1]: + pubdates[1] = pubdates[1].replace(",", "-") + rule_htmls.append( + '发布时间: %s %s小时' + % (rule_filter_string.get(pubdates[0]), pubdates[1])) + if rules.get("upspeed"): + rule_htmls.append('上传限速: %sB/s' + % StringUtils.str_filesize(int(rules.get("upspeed")) * 1024)) + if rules.get("downspeed"): + rule_htmls.append('下载限速: %sB/s' + % StringUtils.str_filesize(int(rules.get("downspeed")) * 1024)) + if rules.get("include"): + rule_htmls.append( + '包含: %s' + % rules.get("include")) + if rules.get("hr"): + rule_htmls.append( + '排除: HR') + if rules.get("exclude"): + rule_htmls.append( + '排除: %s' + % rules.get("exclude")) + if rules.get("dlcount"): + rule_htmls.append('同时下载: %s' + % rules.get("dlcount")) + if rules.get("peercount"): + peer_counts = None + if rules.get("peercount") == "#": + peer_counts = None + elif "#" in rules.get("peercount"): + peer_counts = rules.get("peercount").split("#") + peer_counts[1] = peer_counts[1].replace(",", "-") if (len(peer_counts) >= 2 and peer_counts[1]) else \ + peer_counts[1] + else: + try: + # 兼容性代码 + peer_counts = ["lt", int(rules.get("peercount"))] + except Exception as err: + ExceptionUtils.exception_traceback(err) + pass + if peer_counts: + rule_htmls.append( + '做种人数: %s %s' + % (rule_filter_string.get(peer_counts[0]), peer_counts[1])) + if rules.get("time"): + times = rules.get("time").split("#") + if times[0]: + rule_htmls.append( + '做种时间: %s %s小时' + % (rule_filter_string.get(times[0]), times[1])) + if rules.get("ratio"): + ratios = rules.get("ratio").split("#") + if ratios[0]: + rule_htmls.append( + '分享率: %s %s' + % (rule_filter_string.get(ratios[0]), ratios[1])) + if rules.get("uploadsize"): + uploadsizes = rules.get("uploadsize").split("#") + if uploadsizes[0]: + rule_htmls.append( + '上传量: %s %sGB' + % (rule_filter_string.get(uploadsizes[0]), uploadsizes[1])) + if rules.get("dltime"): + dltimes = rules.get("dltime").split("#") + if dltimes[0]: + rule_htmls.append( + '下载耗时: %s %s小时' + % (rule_filter_string.get(dltimes[0]), dltimes[1])) + if rules.get("avg_upspeed"): + avg_upspeeds = rules.get("avg_upspeed").split("#") + if avg_upspeeds[0]: + rule_htmls.append( + '平均上传速度: %s %sKB/S' + % (rule_filter_string.get(avg_upspeeds[0]), avg_upspeeds[1])) + if rules.get("iatime"): + iatimes = rules.get("iatime").split("#") + if iatimes[0]: + rule_htmls.append( + '未活动时间: %s %s小时' + % (rule_filter_string.get(iatimes[0]), iatimes[1])) + + return "
".join(rule_htmls) + + @staticmethod + def __clear_tmdb_cache(data): + """ + 清空TMDB缓存 + """ + try: + MetaHelper().clear_meta_data() + os.remove(MetaHelper().get_meta_data_path()) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 0, "msg": str(e)} + return {"code": 0} + + @staticmethod + def __check_site_attr(data): + """ + 检查站点标识 + """ + site_attr = Sites().get_grapsite_conf(data.get("url")) + site_free = site_2xfree = site_hr = False + if site_attr.get("FREE"): + site_free = True + if site_attr.get("2XFREE"): + site_2xfree = True + if site_attr.get("HR"): + site_hr = True + return {"code": 0, "site_free": site_free, "site_2xfree": site_2xfree, "site_hr": site_hr} + + @staticmethod + def __refresh_process(data): + """ + 刷新进度条 + """ + detail = ProgressHelper().get_process(data.get("type")) + if detail: + return {"code": 0, "value": detail.get("value"), "text": detail.get("text")} + else: + return {"code": 1, "value": 0, "text": "正在处理..."} + + @staticmethod + def __restory_backup(data): + """ + 解压恢复备份文件 + """ + filename = data.get("file_name") + if filename: + config_path = Config().get_config_path() + temp_path = Config().get_temp_path() + file_path = os.path.join(temp_path, filename) + try: + shutil.unpack_archive(file_path, config_path, format='zip') + return {"code": 0, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + finally: + if os.path.exists(file_path): + os.remove(file_path) + + return {"code": 1, "msg": "文件不存在"} + + @staticmethod + def __start_mediasync(data): + """ + 开始媒体库同步 + """ + ThreadHelper().start_thread(MediaServer().sync_mediaserver, ()) + return {"code": 0} + + @staticmethod + def __mediasync_state(data): + """ + 获取媒体库同步数据情况 + """ + status = MediaServer().get_mediasync_status() + if not status: + return {"code": 0, "text": "未同步"} + else: + return {"code": 0, "text": "电影:%s,电视剧:%s,同步时间:%s" % + (status.get("movie_count"), + status.get("tv_count"), + status.get("time"))} + + @staticmethod + def __get_tvseason_list(data): + """ + 获取剧集季列表 + """ + tmdbid = data.get("tmdbid") + title = data.get("title") + if title: + title_season = MetaInfo(title=title).begin_season + else: + title_season = None + if not str(tmdbid).isdigit(): + media_info = WebUtils.get_mediainfo_from_id(mtype=MediaType.TV, + mediaid=tmdbid) + season_infos = Media().get_tmdb_tv_seasons(media_info.tmdb_info) + else: + season_infos = Media().get_tmdb_tv_seasons_byid(tmdbid=tmdbid) + if title_season: + seasons = [ + { + "text": "第%s季" % title_season, + "num": title_season + } + ] + else: + seasons = [ + { + "text": "第%s季" % cn2an.an2cn(season.get("season_number"), mode='low'), + "num": season.get("season_number") + } + for season in season_infos + ] + return {"code": 0, "seasons": seasons} + + @staticmethod + def __get_userrss_task(data): + """ + 获取自定义订阅详情 + """ + taskid = data.get("id") + return {"code": 0, "detail": RssChecker().get_rsstask_info(taskid=taskid)} + + def __delete_userrss_task(self, data): + """ + 删除自定义订阅 + """ + if self.dbhelper.delete_userrss_task(data.get("id")): + RssChecker().init_config() + return {"code": 0} + else: + return {"code": 1} + + def __update_userrss_task(self, data): + """ + 新增或修改自定义订阅 + """ + uses = data.get("uses") + params = { + "id": data.get("id"), + "name": data.get("name"), + "address": data.get("address"), + "parser": data.get("parser"), + "interval": data.get("interval"), + "uses": uses, + "include": data.get("include"), + "exclude": data.get("exclude"), + "filter_rule": data.get("rule"), + "state": data.get("state"), + "save_path": data.get("save_path"), + "download_setting": data.get("download_setting"), + } + if uses == "D": + params.update({ + "recognization": data.get("recognization") + }) + elif uses == "R": + params.update({ + "over_edition": data.get("over_edition"), + "sites": data.get("sites"), + "filter_args": { + "restype": data.get("restype"), + "pix": data.get("pix"), + "team": data.get("team") + } + }) + else: + return {"code": 1} + if self.dbhelper.update_userrss_task(params): + RssChecker().init_config() + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __get_rssparser(data): + """ + 获取订阅解析器详情 + """ + pid = data.get("id") + return {"code": 0, "detail": RssChecker().get_userrss_parser(pid=pid)} + + def __delete_rssparser(self, data): + """ + 删除订阅解析器 + """ + if self.dbhelper.delete_userrss_parser(data.get("id")): + RssChecker().init_config() + return {"code": 0} + else: + return {"code": 1} + + def __update_rssparser(self, data): + """ + 新增或更新订阅解析器 + """ + params = { + "id": data.get("id"), + "name": data.get("name"), + "type": data.get("type"), + "format": data.get("format"), + "params": data.get("params") + } + if self.dbhelper.update_userrss_parser(params): + RssChecker().init_config() + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __run_userrss(data): + RssChecker().check_task_rss(data.get("id")) + return {"code": 0} + + @staticmethod + def __run_brushtask(data): + BrushTask().check_task_rss(data.get("id")) + return {"code": 0} + + @staticmethod + def __list_site_resources(data): + resources = Indexer().list_builtin_resources(index_id=data.get("id"), + page=data.get("page"), + keyword=data.get("keyword")) + if not resources: + return {"code": 1, "msg": "获取站点资源出现错误,无法连接到站点!"} + else: + return {"code": 0, "data": resources} + + @staticmethod + def __list_rss_articles(data): + uses = RssChecker().get_rsstask_info(taskid=data.get("id")).get("uses") + articles = RssChecker().get_rss_articles(data.get("id")) + count = len(articles) + if articles: + return {"code": 0, "data": articles, "count": count, "uses": uses} + else: + return {"code": 1, "msg": "未获取到报文"} + + def __rss_article_test(self, data): + taskid = data.get("taskid") + title = data.get("title") + if not taskid: + return {"code": -1} + if not title: + return {"code": -1} + media_info, match_flag, exist_flag = RssChecker( + ).test_rss_articles(taskid=taskid, title=title) + if not media_info: + return {"code": 0, "data": {"name": "无法识别"}} + media_dict = self.mediainfo_dict(media_info) + media_dict.update({"match_flag": match_flag, "exist_flag": exist_flag}) + return {"code": 0, "data": media_dict} + + def __list_rss_history(self, data): + downloads = [] + historys = self.dbhelper.get_userrss_task_history(data.get("id")) + count = len(historys) + for history in historys: + params = { + "title": history.TITLE, + "downloader": history.DOWNLOADER, + "date": history.DATE + } + downloads.append(params) + if downloads: + return {"code": 0, "data": downloads, "count": count} + else: + return {"code": 1, "msg": "无下载记录"} + + @staticmethod + def __rss_articles_check(data): + if not data.get("articles"): + return {"code": 2} + res = RssChecker().check_rss_articles( + flag=data.get("flag"), articles=data.get("articles")) + if res: + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __rss_articles_download(data): + if not data.get("articles"): + return {"code": 2} + res = RssChecker().download_rss_articles( + taskid=data.get("taskid"), articles=data.get("articles")) + if res: + return {"code": 0} + else: + return {"code": 1} + + def __add_custom_word_group(self, data): + try: + tmdb_id = data.get("tmdb_id") + tmdb_type = data.get("tmdb_type") + if tmdb_type == "tv": + if not self.dbhelper.is_custom_word_group_existed(tmdbid=tmdb_id, gtype=2): + tmdb_info = Media().get_tmdb_info(mtype=MediaType.TV, tmdbid=tmdb_id) + if not tmdb_info: + return {"code": 1, "msg": "添加失败,无法查询到TMDB信息"} + self.dbhelper.insert_custom_word_groups(title=tmdb_info.get("name"), + year=tmdb_info.get( + "first_air_date")[0:4], + gtype=2, + tmdbid=tmdb_id, + season_count=tmdb_info.get("number_of_seasons")) + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词组(TMDB ID)已存在"} + elif tmdb_type == "movie": + if not self.dbhelper.is_custom_word_group_existed(tmdbid=tmdb_id, gtype=1): + tmdb_info = Media().get_tmdb_info(mtype=MediaType.MOVIE, tmdbid=tmdb_id) + if not tmdb_info: + return {"code": 1, "msg": "添加失败,无法查询到TMDB信息"} + self.dbhelper.insert_custom_word_groups(title=tmdb_info.get("title"), + year=tmdb_info.get( + "release_date")[0:4], + gtype=1, + tmdbid=tmdb_id, + season_count=0) + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词组(TMDB ID)已存在"} + else: + return {"code": 1, "msg": "无法识别媒体类型"} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + def __delete_custom_word_group(self, data): + try: + gid = data.get("gid") + self.dbhelper.delete_custom_word_group(gid=gid) + WordsHelper().init_config() + return {"code": 0, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + def __add_or_edit_custom_word(self, data): + try: + wid = data.get("id") + gid = data.get("gid") + group_type = data.get("group_type") + replaced = data.get("new_replaced") + replace = data.get("new_replace") + front = data.get("new_front") + back = data.get("new_back") + offset = data.get("new_offset") + whelp = data.get("new_help") + wtype = data.get("type") + season = data.get("season") + enabled = data.get("enabled") + regex = data.get("regex") + # 集数偏移格式检查 + if wtype in ["3", "4"]: + if not re.findall(r'EP', offset): + return {"code": 1, "msg": "偏移集数格式有误"} + if re.findall(r'(?!-|\+|\*|/|[0-9]).', re.sub(r'EP', "", offset)): + return {"code": 1, "msg": "偏移集数格式有误"} + if wid: + self.dbhelper.delete_custom_word(wid=wid) + # 电影 + if group_type == "1": + season = -2 + # 屏蔽 + if wtype == "1": + if not self.dbhelper.is_custom_words_existed(replaced=replaced): + self.dbhelper.insert_custom_word(replaced=replaced, + replace="", + front="", + back="", + offset="", + wtype=wtype, + gid=gid, + season=season, + enabled=enabled, + regex=regex, + whelp=whelp if whelp else "") + WordsHelper().init_config() + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词已存在\n(被替换词:%s)" % replaced} + # 替换 + elif wtype == "2": + if not self.dbhelper.is_custom_words_existed(replaced=replaced): + self.dbhelper.insert_custom_word(replaced=replaced, + replace=replace, + front="", + back="", + offset="", + wtype=wtype, + gid=gid, + season=season, + enabled=enabled, + regex=regex, + whelp=whelp if whelp else "") + WordsHelper().init_config() + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词已存在\n(被替换词:%s)" % replaced} + # 集偏移 + elif wtype == "4": + if not self.dbhelper.is_custom_words_existed(front=front, back=back): + self.dbhelper.insert_custom_word(replaced="", + replace="", + front=front, + back=back, + offset=offset, + wtype=wtype, + gid=gid, + season=season, + enabled=enabled, + regex=regex, + whelp=whelp if whelp else "") + WordsHelper().init_config() + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词已存在\n(前后定位词:%s@%s)" % (front, back)} + # 替换+集偏移 + elif wtype == "3": + if not self.dbhelper.is_custom_words_existed(replaced=replaced): + self.dbhelper.insert_custom_word(replaced=replaced, + replace=replace, + front=front, + back=back, + offset=offset, + wtype=wtype, + gid=gid, + season=season, + enabled=enabled, + regex=regex, + whelp=whelp if whelp else "") + WordsHelper().init_config() + return {"code": 0, "msg": ""} + else: + return {"code": 1, "msg": "识别词已存在\n(被替换词:%s)" % replaced} + else: + return {"code": 1, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + def __get_custom_word(self, data): + try: + wid = data.get("wid") + word_info = self.dbhelper.get_custom_words(wid=wid) + if word_info: + word_info = word_info[0] + word = {"id": word_info.ID, + "replaced": word_info.REPLACED, + "replace": word_info.REPLACE, + "front": word_info.FRONT, + "back": word_info.BACK, + "offset": word_info.OFFSET, + "type": word_info.TYPE, + "group_id": word_info.GROUP_ID, + "season": word_info.SEASON, + "enabled": word_info.ENABLED, + "regex": word_info.REGEX, + "help": word_info.HELP, } + else: + word = {} + return {"code": 0, "data": word} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": "查询识别词失败"} + + def __delete_custom_word(self, data): + try: + wid = data.get("id") + self.dbhelper.delete_custom_word(wid) + WordsHelper().init_config() + return {"code": 0, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + def __check_custom_words(self, data): + try: + flag_dict = {"enable": 1, "disable": 0} + ids_info = data.get("ids_info") + enabled = flag_dict.get(data.get("flag")) + ids = [id_info.split("_")[1] for id_info in ids_info] + for wid in ids: + self.dbhelper.check_custom_word(wid=wid, enabled=enabled) + WordsHelper().init_config() + return {"code": 0, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": "识别词状态设置失败"} + + def __export_custom_words(self, data): + try: + note = data.get("note") + ids_info = data.get("ids_info").split("@") + group_ids = [] + word_ids = [] + for id_info in ids_info: + wid = id_info.split("_") + group_ids.append(wid[0]) + word_ids.append(wid[1]) + export_dict = {} + for group_id in group_ids: + if group_id == "-1": + export_dict["-1"] = {"id": -1, + "title": "通用", + "type": 1, + "words": {}, } + else: + group_info = self.dbhelper.get_custom_word_groups( + gid=group_id) + if group_info: + group_info = group_info[0] + export_dict[str(group_info.ID)] = {"id": group_info.ID, + "title": group_info.TITLE, + "year": group_info.YEAR, + "type": group_info.TYPE, + "tmdbid": group_info.TMDBID, + "season_count": group_info.SEASON_COUNT, + "words": {}, } + for word_id in word_ids: + word_info = self.dbhelper.get_custom_words(wid=word_id) + if word_info: + word_info = word_info[0] + export_dict[str(word_info.GROUP_ID)]["words"][str(word_info.ID)] = {"id": word_info.ID, + "replaced": word_info.REPLACED, + "replace": word_info.REPLACE, + "front": word_info.FRONT, + "back": word_info.BACK, + "offset": word_info.OFFSET, + "type": word_info.TYPE, + "season": word_info.SEASON, + "regex": word_info.REGEX, + "help": word_info.HELP, } + export_string = json.dumps(export_dict) + "@@@@@@" + str(note) + string = base64.b64encode( + export_string.encode("utf-8")).decode('utf-8') + return {"code": 0, "string": string} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + @staticmethod + def __analyse_import_custom_words_code(data): + try: + import_code = data.get('import_code') + string = base64.b64decode(import_code.encode( + "utf-8")).decode('utf-8').split("@@@@@@") + note_string = string[1] + import_dict = json.loads(string[0]) + groups = [] + for group in import_dict.values(): + wid = group.get('id') + title = group.get("title") + year = group.get("year") + wtype = group.get("type") + tmdbid = group.get("tmdbid") + season_count = group.get("season_count") or "" + words = group.get("words") + if tmdbid: + link = "https://www.themoviedb.org/%s/%s" % ( + "movie" if int(wtype) == 1 else "tv", tmdbid) + else: + link = "" + groups.append({"id": wid, + "name": "%s(%s)" % (title, year) if year else title, + "link": link, + "type": wtype, + "seasons": season_count, + "words": words}) + return {"code": 0, "groups": groups, "note_string": note_string} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + def __import_custom_words(self, data): + try: + import_code = data.get('import_code') + ids_info = data.get('ids_info') + string = base64.b64decode(import_code.encode( + "utf-8")).decode('utf-8').split("@@@@@@") + import_dict = json.loads(string[0]) + import_group_ids = [id_info.split("_")[0] for id_info in ids_info] + group_id_dict = {} + for import_group_id in import_group_ids: + import_group_info = import_dict.get(import_group_id) + if int(import_group_info.get("id")) == -1: + group_id_dict["-1"] = -1 + continue + title = import_group_info.get("title") + year = import_group_info.get("year") + gtype = import_group_info.get("type") + tmdbid = import_group_info.get("tmdbid") + season_count = import_group_info.get("season_count") + if not self.dbhelper.is_custom_word_group_existed(tmdbid=tmdbid, gtype=gtype): + self.dbhelper.insert_custom_word_groups(title=title, + year=year, + gtype=gtype, + tmdbid=tmdbid, + season_count=season_count) + group_info = self.dbhelper.get_custom_word_groups( + tmdbid=tmdbid, gtype=gtype) + if group_info: + group_id_dict[import_group_id] = group_info[0].ID + for id_info in ids_info: + id_info = id_info.split('_') + import_group_id = id_info[0] + import_word_id = id_info[1] + import_word_info = import_dict.get( + import_group_id).get("words").get(import_word_id) + gid = group_id_dict.get(import_group_id) + replaced = import_word_info.get("replaced") + replace = import_word_info.get("replace") + front = import_word_info.get("front") + back = import_word_info.get("back") + offset = import_word_info.get("offset") + whelp = import_word_info.get("help") + wtype = int(import_word_info.get("type")) + season = import_word_info.get("season") + regex = import_word_info.get("regex") + # 屏蔽, 替换, 替换+集偏移 + if wtype in [1, 2, 3]: + if self.dbhelper.is_custom_words_existed(replaced=replaced): + return {"code": 1, "msg": "识别词已存在\n(被替换词:%s)" % replaced} + # 集偏移 + elif wtype == 4: + if self.dbhelper.is_custom_words_existed(front=front, back=back): + return {"code": 1, "msg": "识别词已存在\n(前后定位词:%s@%s)" % (front, back)} + self.dbhelper.insert_custom_word(replaced=replaced, + replace=replace, + front=front, + back=back, + offset=offset, + wtype=wtype, + gid=gid, + season=season, + enabled=1, + regex=regex, + whelp=whelp if whelp else "") + WordsHelper().init_config() + return {"code": 0, "msg": ""} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e)} + + @staticmethod + def __get_categories(data): + if data.get("type") == "电影": + categories = Category().get_movie_categorys() + elif data.get("type") == "电视剧": + categories = Category().get_tv_categorys() + else: + categories = Category().get_anime_categorys() + return {"code": 0, "category": list(categories), "id": data.get("id"), "value": data.get("value")} + + def __delete_rss_history(self, data): + rssid = data.get("rssid") + self.dbhelper.delete_rss_history(rssid=rssid) + return {"code": 0} + + def __re_rss_history(self, data): + rssid = data.get("rssid") + rtype = data.get("type") + rssinfo = self.dbhelper.get_rss_history(rtype=rtype, rid=rssid) + if rssinfo: + if rtype == "MOV": + mtype = MediaType.MOVIE + else: + mtype = MediaType.TV + if rssinfo[0].SEASON: + season = int(str(rssinfo[0].SEASON).replace("S", "")) + else: + season = None + code, msg, _ = Subscribe().add_rss_subscribe(mtype=mtype, + name=rssinfo[0].NAME, + year=rssinfo[0].YEAR, + season=season, + mediaid=rssinfo[0].TMDBID, + total_ep=rssinfo[0].TOTAL, + current_ep=rssinfo[0].START) + return {"code": code, "msg": msg} + else: + return {"code": 1, "msg": "订阅历史记录不存在"} + + def __share_filtergroup(self, data): + gid = data.get("id") + group_info = self.dbhelper.get_config_filter_group(gid=gid) + if not group_info: + return {"code": 1, "msg": "规则组不存在"} + group_rules = self.dbhelper.get_config_filter_rule(groupid=gid) + if not group_rules: + return {"code": 1, "msg": "规则组没有对应规则"} + rules = [] + for rule in group_rules: + rules.append({ + "name": rule.ROLE_NAME, + "pri": rule.PRIORITY, + "include": rule.INCLUDE, + "exclude": rule.EXCLUDE, + "size": rule.SIZE_LIMIT, + "free": rule.NOTE + }) + rule_json = { + "name": group_info[0].GROUP_NAME, + "rules": rules + } + json_string = base64.b64encode(json.dumps( + rule_json).encode("utf-8")).decode('utf-8') + return {"code": 0, "string": json_string} + + def __import_filtergroup(self, data): + content = data.get("content") + try: + json_str = base64.b64decode( + str(content).encode("utf-8")).decode('utf-8') + json_obj = json.loads(json_str) + if json_obj: + if not json_obj.get("name"): + return {"code": 1, "msg": "数据格式不正确"} + self.dbhelper.add_filter_group(name=json_obj.get("name")) + group_id = self.dbhelper.get_filter_groupid_by_name( + json_obj.get("name")) + if not group_id: + return {"code": 1, "msg": "数据内容不正确"} + if json_obj.get("rules"): + for rule in json_obj.get("rules"): + self.dbhelper.insert_filter_rule(item={ + "group": group_id, + "name": rule.get("name"), + "pri": rule.get("pri"), + "include": rule.get("include"), + "exclude": rule.get("exclude"), + "size": rule.get("size"), + "free": rule.get("free") + }) + Filter().init_config() + return {"code": 0, "msg": ""} + except Exception as err: + ExceptionUtils.exception_traceback(err) + return {"code": 1, "msg": "数据格式不正确,%s" % str(err)} + + @staticmethod + def get_library_spacesize(data=None): + """ + 查询媒体库存储空间 + """ + # 磁盘空间 + UsedPercent = 0 + TotalSpaceList = [] + media = Config().get_config('media') + if media: + # 电影目录 + movie_paths = media.get('movie_path') + if not isinstance(movie_paths, list): + movie_paths = [movie_paths] + movie_used, movie_total = 0, 0 + for movie_path in movie_paths: + if not movie_path: + continue + used, total = SystemUtils.get_used_of_partition(movie_path) + if "%s-%s" % (used, total) not in TotalSpaceList: + TotalSpaceList.append("%s-%s" % (used, total)) + movie_used += used + movie_total += total + # 电视目录 + tv_paths = media.get('tv_path') + if not isinstance(tv_paths, list): + tv_paths = [tv_paths] + tv_used, tv_total = 0, 0 + for tv_path in tv_paths: + if not tv_path: + continue + used, total = SystemUtils.get_used_of_partition(tv_path) + if "%s-%s" % (used, total) not in TotalSpaceList: + TotalSpaceList.append("%s-%s" % (used, total)) + tv_used += used + tv_total += total + # 动漫目录 + anime_paths = media.get('anime_path') + if not isinstance(anime_paths, list): + anime_paths = [anime_paths] + anime_used, anime_total = 0, 0 + for anime_path in anime_paths: + if not anime_path: + continue + used, total = SystemUtils.get_used_of_partition(anime_path) + if "%s-%s" % (used, total) not in TotalSpaceList: + TotalSpaceList.append("%s-%s" % (used, total)) + anime_used += used + anime_total += total + # 总空间 + TotalSpaceAry = [] + if movie_total not in TotalSpaceAry: + TotalSpaceAry.append(movie_total) + if tv_total not in TotalSpaceAry: + TotalSpaceAry.append(tv_total) + if anime_total not in TotalSpaceAry: + TotalSpaceAry.append(anime_total) + TotalSpace = sum(TotalSpaceAry) + # 已使用空间 + UsedSapceAry = [] + if movie_used not in UsedSapceAry: + UsedSapceAry.append(movie_used) + if tv_used not in UsedSapceAry: + UsedSapceAry.append(tv_used) + if anime_used not in UsedSapceAry: + UsedSapceAry.append(anime_used) + UsedSapce = sum(UsedSapceAry) + # 电影电视使用百分比格式化 + if TotalSpace: + UsedPercent = "%0.1f" % ((UsedSapce / TotalSpace) * 100) + # 总剩余空间 格式化 + FreeSpace = "{:,} TB".format( + round((TotalSpace - UsedSapce) / 1024 / 1024 / 1024 / 1024, 2)) + # 总使用空间 格式化 + UsedSapce = "{:,} TB".format( + round(UsedSapce / 1024 / 1024 / 1024 / 1024, 2)) + # 总空间 格式化 + TotalSpace = "{:,} TB".format( + round(TotalSpace / 1024 / 1024 / 1024 / 1024, 2)) + + return {"code": 0, + "UsedPercent": UsedPercent, + "FreeSpace": FreeSpace, + "UsedSapce": UsedSapce, + "TotalSpace": TotalSpace} + + def get_transfer_statistics(self, data=None): + """ + 查询转移历史统计数据 + """ + MovieChartLabels = [] + MovieNums = [] + TvChartData = {} + TvNums = [] + AnimeNums = [] + for statistic in self.dbhelper.get_transfer_statistics(): + if statistic[0] == "电影": + MovieChartLabels.append(statistic[1]) + MovieNums.append(statistic[2]) + else: + if not TvChartData.get(statistic[1]): + TvChartData[statistic[1]] = {"tv": 0, "anime": 0} + if statistic[0] == "电视剧": + TvChartData[statistic[1]]["tv"] += statistic[2] + elif statistic[0] == "动漫": + TvChartData[statistic[1]]["anime"] += statistic[2] + TvChartLabels = list(TvChartData) + for tv_data in TvChartData.values(): + TvNums.append(tv_data.get("tv")) + AnimeNums.append(tv_data.get("anime")) + + return { + "code": 0, + "MovieChartLabels": MovieChartLabels, + "MovieNums": MovieNums, + "TvChartLabels": TvChartLabels, + "TvNums": TvNums, + "AnimeNums": AnimeNums + } + + @staticmethod + def get_library_mediacount(data=None): + """ + 查询媒体库统计数据 + """ + MediaServerClient = MediaServer() + media_counts = MediaServerClient.get_medias_count() + UserCount = MediaServerClient.get_user_count() + if media_counts: + return { + "code": 0, + "Movie": "{:,}".format(media_counts.get('MovieCount')), + "Series": "{:,}".format(media_counts.get('SeriesCount')), + "Episodes": "{:,}".format(media_counts.get('EpisodeCount')) if media_counts.get( + 'EpisodeCount') else "", + "Music": "{:,}".format(media_counts.get('SongCount')), + "User": UserCount + } + else: + return {"code": -1, "msg": "媒体库服务器连接失败"} + + @staticmethod + def get_library_playhistory(data=None): + """ + 查询媒体库播放记录 + """ + return {"code": 0, "result": MediaServer().get_activity_log(30)} + + def get_search_result(self, data=None): + """ + 查询所有搜索结果 + """ + SearchResults = {} + res = self.dbhelper.get_search_results() + total = len(res) + for item in res: + # 质量(来源、效果)、分辨率 + if item.RES_TYPE: + try: + res_mix = json.loads(item.RES_TYPE) + except Exception as err: + ExceptionUtils.exception_traceback(err) + continue + respix = res_mix.get("respix") or "" + video_encode = res_mix.get("video_encode") or "" + restype = res_mix.get("restype") or "" + reseffect = res_mix.get("reseffect") or "" + else: + restype = "" + respix = "" + reseffect = "" + video_encode = "" + # 分组标识 (来源,分辨率) + group_key = re.sub(r"[-.\s@|]", "", f"{respix}_{restype}").lower() + # 分组信息 + group_info = { + "respix": respix, + "restype": restype, + } + # 种子唯一标识 (大小,质量(来源、效果),制作组组成) + unique_key = re.sub(r"[-.\s@|]", "", + f"{respix}_{restype}_{video_encode}_{reseffect}_{item.SIZE}_{item.OTHERINFO}").lower() + # 标识信息 + unique_info = { + "video_encode": video_encode, + "size": item.SIZE, + "reseffect": reseffect, + "releasegroup": item.OTHERINFO + } + # 结果 + title_string = f"{item.TITLE}" + if item.YEAR: + title_string = f"{title_string} ({item.YEAR})" + # 电视剧季集标识 + mtype = item.TYPE or "" + SE_key = item.ES_STRING if item.ES_STRING and mtype != "MOV" else "MOV" + media_type = {"MOV": "电影", "TV": "电视剧", "ANI": "动漫"}.get(mtype) + # 种子信息 + torrent_item = { + "id": item.ID, + "seeders": item.SEEDERS, + "enclosure": item.ENCLOSURE, + "site": item.SITE, + "torrent_name": item.TORRENT_NAME, + "description": item.DESCRIPTION, + "pageurl": item.PAGEURL, + "uploadvalue": item.UPLOAD_VOLUME_FACTOR, + "downloadvalue": item.DOWNLOAD_VOLUME_FACTOR, + "size": item.SIZE, + "respix": respix, + "restype": restype, + "reseffect": reseffect, + "releasegroup": item.OTHERINFO, + "video_encode": video_encode + } + # 促销 + free_item = { + "value": f"{item.UPLOAD_VOLUME_FACTOR} {item.DOWNLOAD_VOLUME_FACTOR}", + "name": MetaBase.get_free_string(item.UPLOAD_VOLUME_FACTOR, item.DOWNLOAD_VOLUME_FACTOR) + } + # 季 + filter_season = SE_key.split()[0] if SE_key and SE_key not in [ + "MOV", "TV"] else None + # 合并搜索结果 + if SearchResults.get(title_string): + # 种子列表 + result_item = SearchResults[title_string] + torrent_dict = SearchResults[title_string].get("torrent_dict") + SE_dict = torrent_dict.get(SE_key) + if SE_dict: + group = SE_dict.get(group_key) + if group: + unique = group.get("group_torrents").get(unique_key) + if unique: + unique["torrent_list"].append(torrent_item) + group["group_total"] += 1 + else: + group["group_total"] += 1 + group.get("group_torrents")[unique_key] = { + "unique_info": unique_info, + "torrent_list": [torrent_item] + } + else: + SE_dict[group_key] = { + "group_info": group_info, + "group_total": 1, + "group_torrents": { + unique_key: { + "unique_info": unique_info, + "torrent_list": [torrent_item] + } + } + } + else: + torrent_dict[SE_key] = { + group_key: { + "group_info": group_info, + "group_total": 1, + "group_torrents": { + unique_key: { + "unique_info": unique_info, + "torrent_list": [torrent_item] + } + } + } + } + # 过滤条件 + torrent_filter = dict(result_item.get("filter")) + if free_item not in torrent_filter.get("free"): + torrent_filter["free"].append(free_item) + if item.SITE not in torrent_filter.get("site"): + torrent_filter["site"].append(item.SITE) + if video_encode \ + and video_encode not in torrent_filter.get("video"): + torrent_filter["video"].append(video_encode) + if filter_season \ + and filter_season not in torrent_filter.get("season"): + torrent_filter["season"].append(filter_season) + else: + # 是否已存在 + if item.TMDBID: + exist_flag = MediaServer().check_item_exists( + title=item.TITLE, year=item.YEAR, tmdbid=item.TMDBID) + else: + exist_flag = False + SearchResults[title_string] = { + "key": item.ID, + "title": item.TITLE, + "year": item.YEAR, + "type_key": mtype, + "image": item.IMAGE, + "type": media_type, + "vote": item.VOTE, + "tmdbid": item.TMDBID, + "backdrop": item.IMAGE, + "poster": item.POSTER, + "overview": item.OVERVIEW, + "exist": exist_flag, + "torrent_dict": { + SE_key: { + group_key: { + "group_info": group_info, + "group_total": 1, + "group_torrents": { + unique_key: { + "unique_info": unique_info, + "torrent_list": [torrent_item] + } + } + } + } + }, + "filter": { + "site": [item.SITE], + "free": [free_item], + "video": [video_encode] if video_encode else [], + "season": [filter_season] if filter_season else [] + } + } + + # 提升整季的顺序到顶层 + def se_sort(k): + k = re.sub(r" +|(?<=s\d)\D*?(?=e)|(?<=s\d\d)\D*?(?=e)", + " ", k[0], flags=re.I).split() + return (k[0], k[1]) if len(k) > 1 else ("Z" + k[0], "ZZZ") + + # 开始排序季集顺序 + for title, item in SearchResults.items(): + # 排序筛选器 季 + item["filter"]["season"].sort(reverse=True) + # 排序种子列 集 + item["torrent_dict"] = sorted(item["torrent_dict"].items(), + key=se_sort, + reverse=True) + return {"code": 0, "total": total, "result": SearchResults} + + @staticmethod + def search_media_infos(data): + """ + 根据关键字搜索相似词条 + """ + SearchWord = data.get("keyword") + if not SearchWord: + return [] + SearchSourceType = data.get("searchtype") + medias = WebUtils.search_media_infos(keyword=SearchWord, + source=SearchSourceType) + + return {"code": 0, "result": [media.to_dict() for media in medias]} + + @staticmethod + def get_movie_rss_list(data=None): + """ + 查询所有电影订阅 + """ + return {"code": 0, "result": Subscribe().get_subscribe_movies()} + + @staticmethod + def get_tv_rss_list(data=None): + """ + 查询所有电视剧订阅 + """ + return {"code": 0, "result": Subscribe().get_subscribe_tvs()} + + def get_rss_history(self, data): + """ + 查询所有订阅历史 + """ + mtype = data.get("type") + return {"code": 0, "result": [rec.as_dict() for rec in self.dbhelper.get_rss_history(rtype=mtype)]} + + @staticmethod + def get_downloading(data=None): + """ + 查询正在下载的任务 + """ + torrents = Downloader().get_downloading_progress() + MediaHander = Media() + for torrent in torrents: + # 识别 + name = torrent.get("name") + media_info = MediaHander.get_media_info(title=name) + if not media_info: + torrent.update({ + "title": name, + "image": "" + }) + continue + if not media_info.tmdb_info: + year = media_info.year + if year: + title = "%s (%s) %s" % (media_info.get_name(), + year, media_info.get_season_episode_string()) + else: + title = "%s %s" % (media_info.get_name(), + media_info.get_season_episode_string()) + else: + title = "%s %s" % (media_info.get_title_string( + ), media_info.get_season_episode_string()) + poster_path = media_info.get_poster_image() + torrent.update({ + "title": title, + "image": poster_path or "" + }) + return {"code": 0, "result": torrents} + + def get_transfer_history(self, data): + """ + 查询媒体整理历史记录 + """ + PageNum = data.get("pagenum") + if not PageNum: + PageNum = 30 + SearchStr = data.get("keyword") + CurrentPage = data.get("page") + if not CurrentPage: + CurrentPage = 1 + else: + CurrentPage = int(CurrentPage) + totalCount, historys = self.dbhelper.get_transfer_history( + SearchStr, CurrentPage, PageNum) + historys_list = [] + for history in historys: + history = history.as_dict() + sync_mode = history.get("MODE") + rmt_mode = ModuleConf.get_dictenum_key( + ModuleConf.RMT_MODES, sync_mode) if sync_mode else "" + history.update({ + "SYNC_MODE": sync_mode, + "RMT_MODE": rmt_mode + }) + historys_list.append(history) + TotalPage = floor(totalCount / PageNum) + 1 + + return { + "code": 0, + "total": totalCount, + "result": historys_list, + "totalPage": TotalPage, + "pageNum": PageNum, + "currentPage": CurrentPage + } + + def get_unknown_list(self, data=None): + """ + 查询所有未识别记录 + """ + Items = [] + Records = self.dbhelper.get_transfer_unknown_paths() + for rec in Records: + if not rec.PATH: + continue + path = rec.PATH.replace("\\", "/") if rec.PATH else "" + path_to = rec.DEST.replace("\\", "/") if rec.DEST else "" + sync_mode = rec.MODE or "" + rmt_mode = ModuleConf.get_dictenum_key(ModuleConf.RMT_MODES, + sync_mode) if sync_mode else "" + Items.append({ + "id": rec.ID, + "path": path, + "to": path_to, + "name": path, + "sync_mode": sync_mode, + "rmt_mode": rmt_mode, + }) + + return {"code": 0, "items": Items} + + def unidentification(self): + """ + 重新识别所有未识别记录 + """ + ItemIds = [] + Records = self.dbhelper.get_transfer_unknown_paths() + for rec in Records: + if not rec.PATH: + continue + ItemIds.append(rec.ID) + + if len(ItemIds) > 0: + WebAction.re_identification(self, {"flag": "unidentification", "ids": ItemIds}) + + def get_customwords(self, data=None): + words = [] + words_info = self.dbhelper.get_custom_words(gid=-1) + for word_info in words_info: + words.append({"id": word_info.ID, + "replaced": word_info.REPLACED, + "replace": word_info.REPLACE, + "front": word_info.FRONT, + "back": word_info.BACK, + "offset": word_info.OFFSET, + "type": word_info.TYPE, + "group_id": word_info.GROUP_ID, + "season": word_info.SEASON, + "enabled": word_info.ENABLED, + "regex": word_info.REGEX, + "help": word_info.HELP, }) + groups = [{"id": "-1", + "name": "通用", + "link": "", + "type": "1", + "seasons": "0", + "words": words}] + groups_info = self.dbhelper.get_custom_word_groups() + for group_info in groups_info: + gid = group_info.ID + name = "%s (%s)" % (group_info.TITLE, group_info.YEAR) + gtype = group_info.TYPE + if gtype == 1: + link = "https://www.themoviedb.org/movie/%s" % group_info.TMDBID + else: + link = "https://www.themoviedb.org/tv/%s" % group_info.TMDBID + words = [] + words_info = self.dbhelper.get_custom_words(gid=gid) + for word_info in words_info: + words.append({"id": word_info.ID, + "replaced": word_info.REPLACED, + "replace": word_info.REPLACE, + "front": word_info.FRONT, + "back": word_info.BACK, + "offset": word_info.OFFSET, + "type": word_info.TYPE, + "group_id": word_info.GROUP_ID, + "season": word_info.SEASON, + "enabled": word_info.ENABLED, + "regex": word_info.REGEX, + "help": word_info.HELP, }) + groups.append({"id": gid, + "name": name, + "link": link, + "type": group_info.TYPE, + "seasons": group_info.SEASON_COUNT, + "words": words}) + return { + "code": 0, + "result": groups + } + + def get_directorysync(self, data=None): + """ + 查询所有同步目录 + """ + sync_paths = self.dbhelper.get_config_sync_paths() + SyncPaths = [] + if sync_paths: + for sync_item in sync_paths: + SyncPath = {'id': sync_item.ID, + 'from': sync_item.SOURCE, + 'to': sync_item.DEST or "", + 'unknown': sync_item.UNKNOWN or "", + 'syncmod': sync_item.MODE, + 'syncmod_name': RmtMode[sync_item.MODE.upper()].value, + 'rename': sync_item.RENAME, + 'enabled': sync_item.ENABLED} + SyncPaths.append(SyncPath) + SyncPaths = sorted(SyncPaths, key=lambda o: o.get("from")) + return {"code": 0, "result": SyncPaths} + + def get_users(self, data=None): + """ + 查询所有用户 + """ + user_list = self.dbhelper.get_users() + Users = [] + for user in user_list: + pris = str(user.PRIS).split(",") + Users.append({"id": user.ID, "name": user.NAME, "pris": pris}) + return {"code": 0, "result": Users} + + @staticmethod + def get_filterrules(data=None): + """ + 查询所有过滤规则 + """ + RuleGroups = Filter().get_rule_infos() + sql_file = os.path.join(Config().get_script_path(), "init_filter.sql") + with open(sql_file, "r", encoding="utf-8") as f: + sql_list = f.read().split(';\n') + Init_RuleGroups = [] + i = 0 + while i < len(sql_list): + rulegroup = {} + rulegroup_info = re.findall( + r"[0-9]+,'[^\"]+NULL", sql_list[i], re.I)[0].split(",") + rulegroup['id'] = int(rulegroup_info[0]) + rulegroup['name'] = rulegroup_info[1][1:-1] + rulegroup['rules'] = [] + rulegroup['sql'] = [sql_list[i]] + if i + 1 < len(sql_list): + rules = re.findall( + r"[0-9]+,'[^\"]+NULL", sql_list[i + 1], re.I)[0].split("),\n (") + for rule in rules: + rule_info = {} + rule = rule.split(",") + rule_info['name'] = rule[2][1:-1] + rule_info['include'] = rule[4][1:-1] + rule_info['exclude'] = rule[5][1:-1] + rulegroup['rules'].append(rule_info) + rulegroup["sql"].append(sql_list[i + 1]) + Init_RuleGroups.append(rulegroup) + i = i + 2 + return { + "code": 0, + "ruleGroups": RuleGroups, + "initRules": Init_RuleGroups + } + + def __update_directory(self, data): + """ + 维护媒体库目录 + """ + cfg = self.set_config_directory(Config().get_config(), + data.get("oper"), + data.get("key"), + data.get("value"), + data.get("replace_value")) + # 保存配置 + Config().save_config(cfg) + return {"code": 0} + + @staticmethod + def __test_site(data): + """ + 测试站点连通性 + """ + flag, msg, times = Sites().test_connection(data.get("id")) + code = 0 if flag else -1 + return {"code": code, "msg": msg, "time": times} + + @staticmethod + def __get_sub_path(data): + """ + 查询下级子目录 + """ + r = [] + try: + ft = data.get("filter") or "ALL" + d = data.get("dir") + if not d or d == "/": + if SystemUtils.get_system() == OsType.WINDOWS: + partitions = SystemUtils.get_windows_drives() + if partitions: + dirs = [os.path.join(partition, "/") + for partition in partitions] + else: + dirs = [os.path.join("C:/", f) + for f in os.listdir("C:/")] + else: + dirs = [os.path.join("/", f) for f in os.listdir("/")] + else: + d = os.path.normpath(unquote(d)) + if not os.path.isdir(d): + d = os.path.dirname(d) + dirs = [os.path.join(d, f) for f in os.listdir(d)] + dirs.sort() + for ff in dirs: + if os.path.isdir(ff): + if 'ONLYDIR' in ft or 'ALL' in ft: + r.append({ + "path": ff.replace("\\", "/"), + "name": os.path.basename(ff), + "type": "dir", + "rel": os.path.dirname(ff).replace("\\", "/") + }) + else: + ext = os.path.splitext(ff)[-1][1:] + flag = False + if 'ONLYFILE' in ft or 'ALL' in ft: + flag = True + elif "MEDIAFILE" in ft and f".{str(ext).lower()}" in RMT_MEDIAEXT: + flag = True + elif "SUBFILE" in ft and f".{str(ext).lower()}" in RMT_SUBEXT: + flag = True + if flag: + r.append({ + "path": ff.replace("\\", "/"), + "name": os.path.basename(ff), + "type": "file", + "rel": os.path.dirname(ff).replace("\\", "/"), + "ext": ext, + "size": StringUtils.str_filesize(os.path.getsize(ff)) + }) + + except Exception as e: + ExceptionUtils.exception_traceback(e) + return { + "code": -1, + "message": '加载路径失败: %s' % str(e) + } + return { + "code": 0, + "count": len(r), + "data": r + } + + @staticmethod + def __rename_file(data): + """ + 文件重命名 + """ + path = data.get("path") + name = data.get("name") + if path and name: + try: + shutil.move(path, os.path.join(os.path.dirname(path), name)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": -1, "msg": str(e)} + return {"code": 0} + + def __delete_files(self, data): + """ + 删除文件 + """ + files = data.get("files") + if files: + # 删除文件 + for file in files: + del_flag, del_msg = self.delete_media_file(filedir=os.path.dirname(file), + filename=os.path.basename(file)) + if not del_flag: + log.error(f"【MediaFile】{del_msg}") + else: + log.info(f"【MediaFile】{del_msg}") + return {"code": 0} + + @staticmethod + def __download_subtitle(data): + """ + 从配置的字幕服务下载单个文件的字幕 + """ + path = data.get("path") + name = data.get("name") + media = Media().get_media_info(title=name) + if not media or not media.tmdb_info: + return {"code": -1, "msg": f"{name} 无法从TMDB查询到媒体信息"} + if not media.imdb_id: + media.set_tmdb_info(Media().get_tmdb_info(mtype=media.type, + tmdbid=media.tmdb_id)) + subtitle_item = [{"type": media.type, + "file": os.path.splitext(path)[0], + "file_ext": os.path.splitext(name)[-1], + "name": media.en_name if media.en_name else media.cn_name, + "title": media.title, + "year": media.year, + "season": media.begin_season, + "episode": media.begin_episode, + "bluray": False, + "imdbid": media.imdb_id}] + success, retmsg = Subtitle().download_subtitle(items=subtitle_item) + if success: + return {"code": 0, "msg": retmsg} + else: + return {"code": -1, "msg": retmsg} + + @staticmethod + def __get_download_setting(data): + sid = data.get("sid") + if sid: + download_setting = Downloader().get_download_setting(sid=sid) + else: + download_setting = list( + Downloader().get_download_setting().values()) + return {"code": 0, "data": download_setting} + + def __update_download_setting(self, data): + sid = data.get("sid") + name = data.get("name") + category = data.get("category") + tags = data.get("tags") + content_layout = data.get("content_layout") + is_paused = data.get("is_paused") + upload_limit = data.get("upload_limit") + download_limit = data.get("download_limit") + ratio_limit = data.get("ratio_limit") + seeding_time_limit = data.get("seeding_time_limit") + downloader = data.get("downloader") + self.dbhelper.update_download_setting(sid=sid, + name=name, + category=category, + tags=tags, + content_layout=content_layout, + is_paused=is_paused, + upload_limit=upload_limit or 0, + download_limit=download_limit or 0, + ratio_limit=ratio_limit or 0, + seeding_time_limit=seeding_time_limit or 0, + downloader=downloader) + Downloader().init_config() + return {"code": 0} + + def __delete_download_setting(self, data): + sid = data.get("sid") + self.dbhelper.delete_download_setting(sid=sid) + Downloader().init_config() + return {"code": 0} + + def __update_message_client(self, data): + """ + 更新消息设置 + """ + name = data.get("name") + cid = data.get("cid") + ctype = data.get("type") + config = data.get("config") + switchs = data.get("switchs") + interactive = data.get("interactive") + enabled = data.get("enabled") + if cid: + self.dbhelper.delete_message_client(cid=cid) + self.dbhelper.insert_message_client(name=name, + ctype=ctype, + config=config, + switchs=switchs, + interactive=interactive, + enabled=enabled) + Message().init_config() + return {"code": 0} + + def __delete_message_client(self, data): + """ + 删除消息设置 + """ + if self.dbhelper.delete_message_client(cid=data.get("cid")): + Message().init_config() + return {"code": 0} + else: + return {"code": 1} + + def __check_message_client(self, data): + """ + 维护消息设置 + """ + flag = data.get("flag") + cid = data.get("cid") + ctype = data.get("type") + checked = data.get("checked") + if flag == "interactive": + # TG/WX只能开启一个交互 + if checked: + self.dbhelper.check_message_client(interactive=0, ctype=ctype) + self.dbhelper.check_message_client(cid=cid, + interactive=1 if checked else 0) + Message().init_config() + return {"code": 0} + elif flag == "enable": + self.dbhelper.check_message_client(cid=cid, + enabled=1 if checked else 0) + Message().init_config() + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __get_message_client(data): + """ + 获取消息设置 + """ + cid = data.get("cid") + return {"code": 0, "detail": Message().get_message_client_info(cid=cid)} + + @staticmethod + def __test_message_client(data): + """ + 测试消息设置 + """ + ctype = data.get("type") + config = json.loads(data.get("config")) + res = Message().get_status(ctype=ctype, config=config) + if res: + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __get_indexers(data=None): + """ + 获取索引器 + """ + return {"code": 0, "indexers": Indexer().get_indexer_dict()} + + @staticmethod + def __get_download_dirs(data): + """ + 获取下载目录 + """ + sid = data.get("sid") + site = data.get("site") + if not sid and site: + sid = Sites().get_site_download_setting(site_name=site) + dirs = Downloader().get_download_dirs(setting=sid) + return {"code": 0, "paths": dirs} + + @staticmethod + def __find_hardlinks(data): + files = data.get("files") + file_dir = data.get("dir") + if not files: + return [] + if not file_dir and os.name != "nt": + # 取根目录下一级为查找目录 + file_dir = os.path.commonpath(files).replace("\\", "/") + if file_dir != "/": + file_dir = "/" + str(file_dir).split("/")[1] + else: + return [] + hardlinks = {} + if files: + try: + for file in files: + hardlinks[os.path.basename(file)] = SystemUtils( + ).find_hardlinks(file=file, fdir=file_dir) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1} + return {"code": 0, "data": hardlinks} + + @staticmethod + def __update_sites_cookie_ua(data): + """ + 更新所有站点的Cookie和UA + """ + siteid = data.get("siteid") + username = data.get("username") + password = data.get("password") + twostepcode = data.get("two_step_code") + ocrflag = data.get("ocrflag") + # 保存设置 + SystemConfig().set_system_config(key="CookieUserInfo", + value={ + "username": username, + "password": password, + "two_step_code": twostepcode + }) + retcode, messages = SiteCookie().update_sites_cookie_ua(siteid=siteid, + username=username, + password=password, + twostepcode=twostepcode, + ocrflag=ocrflag) + if retcode == 0: + Sites().init_config() + return {"code": retcode, "messages": messages} + + @staticmethod + def __set_site_captcha_code(data): + """ + 设置站点验证码 + """ + code = data.get("code") + value = data.get("value") + SiteCookie().set_code(code=code, value=value) + return {"code": 0} + + @staticmethod + def __update_torrent_remove_task(data): + """ + 更新自动删种任务 + """ + flag, msg = TorrentRemover().update_torrent_remove_task(data=data) + if not flag: + return {"code": 1, "msg": msg} + else: + TorrentRemover().init_config() + return {"code": 0} + + @staticmethod + def __get_torrent_remove_task(data=None): + """ + 获取自动删种任务 + """ + if data: + tid = data.get("tid") + else: + tid = None + return {"code": 0, "detail": TorrentRemover().get_torrent_remove_tasks(taskid=tid)} + + @staticmethod + def __delete_torrent_remove_task(data): + """ + 删除自动删种任务 + """ + tid = data.get("tid") + flag = TorrentRemover().delete_torrent_remove_task(taskid=tid) + if flag: + TorrentRemover().init_config() + return {"code": 0} + else: + return {"code": 1} + + @staticmethod + def __get_remove_torrents(data): + """ + 获取满足自动删种任务的种子 + """ + tid = data.get("tid") + flag, torrents = TorrentRemover().get_remove_torrents(taskid=tid) + if not flag or not torrents: + return {"code": 1, "msg": "未获取到符合处理条件种子"} + return {"code": 0, "data": torrents} + + @staticmethod + def __auto_remove_torrents(data): + """ + 执行自动删种任务 + """ + tid = data.get("tid") + TorrentRemover().auto_remove_torrents(taskids=tid) + return {"code": 0} + + @staticmethod + def __get_site_favicon(data): + """ + 获取站点图标 + """ + sitename = data.get("name") + return {"code": 0, "icon": Sites().get_site_favicon(site_name=sitename)} + + def get_douban_history(self, data=None): + """ + 查询豆瓣同步历史 + """ + results = self.dbhelper.get_douban_history() + return {"code": 0, "result": [item.as_dict() for item in results]} + + def __delete_douban_history(self, data): + """ + 删除豆瓣同步历史 + """ + self.dbhelper.delete_douban_history(data.get("id")) + return {"code": 0} + + def __list_brushtask_torrents(self, data): + """ + 获取刷流任务的种子明细 + """ + results = self.dbhelper.get_brushtask_torrents(brush_id=data.get("id"), + active=False) + if not results: + return {"code": 1, "msg": "未下载种子或未获取到种子明细"} + return {"code": 0, "data": [item.as_dict() for item in results]} + + @staticmethod + def __set_system_config(data): + """ + 设置系统设置(数据库) + """ + key = data.get("key") + value = data.get("value") + if not key or not value: + return {"code": 1} + try: + SystemConfig().set_system_config(key=key, value=value) + if key == "SpeedLimit": + SpeedLimiter().init_config() + return {"code": 0} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1} + + @staticmethod + def get_site_user_statistics(data): + """ + 获取站点用户统计信息 + """ + sites = data.get("sites") + encoding = data.get("encoding") or "RAW" + sort_by = data.get("sort_by") + sort_on = data.get("sort_on") + site_hash = data.get("site_hash") + statistics = SiteUserInfo().get_site_user_statistics(sites=sites, encoding=encoding) + if sort_by and sort_on in ["asc", "desc"]: + if sort_on == "asc": + statistics.sort(key=lambda x: x[sort_by]) + else: + statistics.sort(key=lambda x: x[sort_by], reverse=True) + if site_hash == "Y": + for item in statistics: + item["site_hash"] = StringUtils.md5_hash(item.get("site")) + return {"code": 0, "data": statistics} + + @staticmethod + def send_custom_message(data): + """ + 发送自定义消息 + """ + title = data.get("title") + text = data.get("text") or "" + image = data.get("image") or "" + Message().send_custom_message(title=title, text=text, image=image) + return {"code": 0} + + @staticmethod + def get_rmt_modes(): + RmtModes = ModuleConf.RMT_MODES_LITE if SystemUtils.is_lite_version( + ) else ModuleConf.RMT_MODES + return [{ + "value": value, + "name": name.value + } for value, name in RmtModes.items()] + + def __cookiecloud_sync(self, data): + """ + CookieCloud数据同步 + """ + server = data.get("server") + key = data.get("key") + password = data.get("password") + # 保存设置 + SystemConfig().set_system_config(key="CookieCloud", + value={ + "server": server, + "key": key, + "password": password + }) + # 同步数据 + contents, retmsg = CookieCloudHelper(server=server, + key=key, + password=password).download_data() + if not contents: + return {"code": 1, "msg": retmsg} + success_count = 0 + for domain, content_list in contents.items(): + if domain.startswith('.'): + domain = domain[1:] + cookie_str = "" + for content in content_list: + cookie_str += content.get("name") + \ + "=" + content.get("value") + ";" + if not cookie_str: + continue + site_info = Sites().get_sites(siteurl=domain) + if not site_info: + continue + self.dbhelper.update_site_cookie_ua(tid=site_info.get("id"), + cookie=cookie_str) + success_count += 1 + if success_count: + # 重载站点信息 + Sites().init_config() + return {"code": 0, "msg": f"成功更新 {success_count} 个站点的Cookie数据"} + return {"code": 0, "msg": "同步完成,但未更新任何站点的Cookie!"} + + @staticmethod + def media_detail(data): + """ + 获取媒体详情 + """ + # TMDBID 或 DB:豆瓣ID + tmdbid = data.get("tmdbid") + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + if not tmdbid: + return {"code": 1, "msg": "未指定媒体ID"} + media_info = WebUtils.get_mediainfo_from_id( + mtype=mtype, mediaid=tmdbid) + # 检查TMDB信息 + if not media_info or not media_info.tmdb_info: + return { + "code": 1, + "msg": "无法查询到TMDB信息" + } + # 查询存在及订阅状态 + fav, rssid = FileTransfer().get_media_exists_flag(mtype=mtype, + title=media_info.title, + year=media_info.year, + mediaid=media_info.tmdb_id) + MediaHander = Media() + return { + "code": 0, + "data": { + "tmdbid": media_info.tmdb_id, + "douban_id": media_info.douban_id, + "background": MediaHander.get_tmdb_backdrops(tmdbinfo=media_info.tmdb_info), + "image": media_info.get_poster_image(), + "vote": media_info.vote_average, + "year": media_info.year, + "title": media_info.title, + "genres": MediaHander.get_tmdb_genres_names(tmdbinfo=media_info.tmdb_info), + "overview": media_info.overview, + "runtime": StringUtils.str_timehours(media_info.runtime), + "fact": MediaHander.get_tmdb_factinfo(media_info), + "crews": MediaHander.get_tmdb_crews(tmdbinfo=media_info.tmdb_info, nums=6), + "actors": MediaHander.get_tmdb_cats(mtype=mtype, tmdbid=media_info.tmdb_id), + "link": media_info.get_detail_url(), + "douban_link": media_info.get_douban_detail_url(), + "fav": fav, + "rssid": rssid + } + } + + @staticmethod + def __media_similar(data): + """ + 查询TMDB相似媒体 + """ + tmdbid = data.get("tmdbid") + page = data.get("page") or 1 + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + if not tmdbid: + return {"code": 1, "msg": "未指定TMDBID"} + if mtype == MediaType.MOVIE: + result = Media().get_movie_similar(tmdbid=tmdbid, page=page) + else: + result = Media().get_tv_similar(tmdbid=tmdbid, page=page) + return {"code": 0, "data": result} + + @staticmethod + def __media_recommendations(data): + """ + 查询TMDB同类推荐媒体 + """ + tmdbid = data.get("tmdbid") + page = data.get("page") or 1 + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + if not tmdbid: + return {"code": 1, "msg": "未指定TMDBID"} + if mtype == MediaType.MOVIE: + result = Media().get_movie_recommendations(tmdbid=tmdbid, page=page) + else: + result = Media().get_tv_recommendations(tmdbid=tmdbid, page=page) + return {"code": 0, "data": result} + + @staticmethod + def __media_person(data): + """ + 查询TMDB媒体所有演员 + """ + tmdbid = data.get("tmdbid") + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + if not tmdbid: + return {"code": 1, "msg": "未指定TMDBID"} + return {"code": 0, "data": Media().get_tmdb_cats(tmdbid=tmdbid, + mtype=mtype)} + + @staticmethod + def __person_medias(data): + """ + 查询演员参演作品 + """ + personid = data.get("personid") + page = data.get("page") or 1 + mtype = MediaType.MOVIE if data.get( + "type") in MovieTypes else MediaType.TV + if not personid: + return {"code": 1, "msg": "未指定演员ID"} + return {"code": 0, "data": Media().get_person_medias(personid=personid, + mtype=mtype, + page=page)} + + @staticmethod + def __save_user_script(data): + """ + 保存用户自定义脚本 + """ + script = data.get("javascript") or "" + css = data.get("css") or "" + SystemConfig().set_system_config(key="CustomScript", + value={ + "css": css, + "javascript": script + }) + return {"code": 0, "msg": "保存成功"} + + @staticmethod + def __run_directory_sync(data): + """ + 执行单个目录的目录同步 + """ + Sync().transfer_all_sync(sid=data.get("sid")) + return {"code": 0, "msg": "执行成功"} diff --git a/web/apiv1.py b/web/apiv1.py new file mode 100644 index 0000000..7f457d4 --- /dev/null +++ b/web/apiv1.py @@ -0,0 +1,2278 @@ +from flask import Blueprint, request +from flask_restx import Api, reqparse, Resource + +from app.brushtask import BrushTask +from app.rsschecker import RssChecker +from app.sites import Sites +from app.utils import TokenCache +from config import Config +from web.action import WebAction +from web.backend.user import User +from web.security import require_auth, login_required, generate_access_token + +apiv1_bp = Blueprint("apiv1", + __name__, + static_url_path='', + static_folder='./frontend/static/', + template_folder='./frontend/', ) +Apiv1 = Api(apiv1_bp, + version="1.0", + title="NAStool Api", + description="POST接口调用 /user/login 获取Token,GET接口使用 基础设置->安全->Api Key 调用", + doc="/", + security='Bearer Auth', + authorizations={"Bearer Auth": {"type": "apiKey", "name": "Authorization", "in": "header"}}, + ) +# API分组 +user = Apiv1.namespace('user', description='用户') +system = Apiv1.namespace('system', description='系统') +config = Apiv1.namespace('config', description='设置') +site = Apiv1.namespace('site', description='站点') +service = Apiv1.namespace('service', description='服务') +subscribe = Apiv1.namespace('subscribe', description='订阅') +rss = Apiv1.namespace('rss', description='自定义RSS') +recommend = Apiv1.namespace('recommend', description='推荐') +search = Apiv1.namespace('search', description='搜索') +download = Apiv1.namespace('download', description='下载') +organization = Apiv1.namespace('organization', description='整理') +torrentremover = Apiv1.namespace('torrentremover', description='自动删种') +library = Apiv1.namespace('library', description='媒体库') +brushtask = Apiv1.namespace('brushtask', description='刷流') +media = Apiv1.namespace('media', description='媒体') +sync = Apiv1.namespace('sync', description='目录同步') +filterrule = Apiv1.namespace('filterrule', description='过滤规则') +words = Apiv1.namespace('words', description='识别词') +message = Apiv1.namespace('message', description='消息通知') +douban = Apiv1.namespace('douban', description='豆瓣') + + +class ApiResource(Resource): + """ + API 认证 + """ + method_decorators = [require_auth] + + +class ClientResource(Resource): + """ + 登录认证 + """ + method_decorators = [login_required] + + +def Failed(): + """ + 返回失败报名 + """ + return { + "code": -1, + "success": False, + "data": {} + } + + +@user.route('/login') +class UserLogin(Resource): + parser = reqparse.RequestParser() + parser.add_argument('username', type=str, help='用户名', location='form', required=True) + parser.add_argument('password', type=str, help='密码', location='form', required=True) + + @user.doc(parser=parser) + def post(self): + """ + 用户登录 + """ + args = self.parser.parse_args() + username = args.get('username') + password = args.get('password') + if not username or not password: + return {"code": 1, "success": False, "message": "用户名或密码错误"} + user_info = User().get_user(username) + if not user_info: + return {"code": 1, "success": False, "message": "用户名或密码错误"} + # 校验密码 + if not user_info.verify_password(password): + return {"code": 1, "success": False, "message": "用户名或密码错误"} + # 缓存Token + token = generate_access_token(username) + TokenCache.set(token, token) + return { + "code": 0, + "success": True, + "data": { + "token": token, + "apikey": Config().get_config("security").get("api_key"), + "userinfo": { + "userid": user_info.id, + "username": user_info.username, + "userpris": str(user_info.pris).split(",") + } + } + } + + +@user.route('/info') +class UserInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('username', type=str, help='用户名', location='form', required=True) + + @user.doc(parser=parser) + def post(self): + """ + 获取用户信息 + """ + args = self.parser.parse_args() + username = args.get('username') + user_info = User().get_user(username) + if not user_info: + return {"code": 1, "success": False, "message": "用户名不正确"} + return { + "code": 0, + "success": True, + "data": { + "userid": user_info.id, + "username": user_info.username, + "userpris": str(user_info.pris).split(",") + } + } + + +@user.route('/manage') +class UserManage(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('oper', type=str, help='操作类型(add 新增/del删除)', location='form', required=True) + parser.add_argument('name', type=str, help='用户名', location='form', required=True) + parser.add_argument('pris', type=str, help='权限', location='form') + + @user.doc(parser=parser) + def post(self): + """ + 用户管理 + """ + return WebAction().api_action(cmd='user_manager', data=self.parser.parse_args()) + + +@user.route('/list') +class UserList(ClientResource): + @staticmethod + def post(): + """ + 查询所有用户 + """ + return WebAction().api_action(cmd='get_users') + + +@service.route('/mediainfo') +class ServiceMediaInfo(ApiResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='名称', location='args', required=True) + + @service.doc(parser=parser) + def get(self): + """ + 识别媒体信息(密钥认证) + """ + return WebAction().api_action(cmd='name_test', data=self.parser.parse_args()) + + +@service.route('/name/test') +class ServiceNameTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='名称', location='form', required=True) + + @service.doc(parser=parser) + def post(self): + """ + 名称识别测试 + """ + return WebAction().api_action(cmd='name_test', data=self.parser.parse_args()) + + +@service.route('/rule/test') +class ServiceRuleTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('title', type=str, help='名称', location='form', required=True) + parser.add_argument('subtitle', type=str, help='描述', location='form') + parser.add_argument('size', type=float, help='大小(GB)', location='form') + + @service.doc(parser=parser) + def post(self): + """ + 过滤规则测试 + """ + return WebAction().api_action(cmd='rule_test', data=self.parser.parse_args()) + + +@service.route('/network/test') +class ServiceNetworkTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('url', type=str, help='URL地址', location='form', required=True) + + @service.doc(parser=parser) + def post(self): + """ + 网络连接性测试 + """ + return WebAction().api_action(cmd='net_test', data=self.parser.parse_args().get("url")) + + +@service.route('/run') +class ServiceRun(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('item', type=str, + help='服务名称(autoremovetorrents、pttransfer、ptsignin、sync、rssdownload、douban、subscribe_search_all)', + location='form', + required=True) + + @service.doc(parser=parser) + def post(self): + """ + 运行服务 + """ + return WebAction().api_action(cmd='sch', data=self.parser.parse_args()) + + +@site.route('/statistics') +class SiteStatistic(ApiResource): + @staticmethod + def get(): + """ + 获取站点数据明细(密钥认证) + """ + # 返回站点信息 + return { + "code": 0, + "success": True, + "data": { + "user_statistics": WebAction().get_site_user_statistics({"encoding": "DICT"}).get("data") + } + } + + +@site.route('/sites') +class SiteSites(ApiResource): + @staticmethod + def get(): + """ + 获取所有站点配置(密钥认证) + """ + return { + "code": 0, + "success": True, + "data": { + "user_sites": Sites().get_sites() + } + } + + +@site.route('/update') +class SiteUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('site_name', type=str, help='站点名称', location='form', required=True) + parser.add_argument('site_id', type=int, help='更新站点ID', location='form') + parser.add_argument('site_pri', type=str, help='优先级', location='form') + parser.add_argument('site_rssurl', type=str, help='RSS地址', location='form') + parser.add_argument('site_signurl', type=str, help='站点地址', location='form') + parser.add_argument('site_cookie', type=str, help='Cookie', location='form') + parser.add_argument('site_note', type=str, help='站点属性', location='form') + parser.add_argument('site_include', type=str, help='站点用途', location='form') + + @site.doc(parser=parser) + def post(self): + """ + 新增/删除站点 + """ + return WebAction().api_action(cmd='update_site', data=self.parser.parse_args()) + + +@site.route('/info') +class SiteInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='站点ID', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 查询单个站点详情 + """ + return WebAction().api_action(cmd='get_site', data=self.parser.parse_args()) + + +@site.route('/favicon') +class SiteFavicon(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='站点名称', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 获取站点图标(Base64) + """ + return WebAction().api_action(cmd='get_site_favicon', data=self.parser.parse_args()) + + +@site.route('/test') +class SiteTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='站点ID', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 测试站点连通性 + """ + return WebAction().api_action(cmd='test_site', data=self.parser.parse_args()) + + +@site.route('/delete') +class SiteDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='站点ID', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 删除站点 + """ + return WebAction().api_action(cmd='del_site', data=self.parser.parse_args()) + + +@site.route('/statistics/activity') +class SiteStatisticsActivity(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='站点名称', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 查询站点 上传/下载/做种数据 + """ + return WebAction().api_action(cmd='get_site_activity', data=self.parser.parse_args()) + + +@site.route('/check') +class SiteCheck(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('url', type=str, help='站点地址', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 检查站点是否支持FREE/HR检测 + """ + return WebAction().api_action(cmd='check_site_attr', data=self.parser.parse_args()) + + +@site.route('/statistics/history') +class SiteStatisticsHistory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('days', type=int, help='时间范围(天)', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 查询所有站点历史数据 + """ + return WebAction().api_action(cmd='get_site_history', data=self.parser.parse_args()) + + +@site.route('/statistics/seedinfo') +class SiteStatisticsSeedinfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='站点名称', location='form', required=True) + + @site.doc(parser=parser) + def post(self): + """ + 查询站点做种分布 + """ + return WebAction().api_action(cmd='get_site_seeding_info', data=self.parser.parse_args()) + + +@site.route('/resources') +class SiteResources(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='站点索引ID', location='form', required=True) + parser.add_argument('page', type=int, help='页码', location='form') + parser.add_argument('keyword', type=str, help='站点名称', location='form') + + @site.doc(parser=parser) + def post(self): + """ + 查询站点资源列表 + """ + return WebAction().api_action(cmd='list_site_resources', data=self.parser.parse_args()) + + +@site.route('/list') +class SiteList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('basic', type=int, help='只查询基本信息(0-否/1-是)', location='form') + parser.add_argument('rss', type=int, help='订阅(0-否/1-是)', location='form') + parser.add_argument('brush', type=int, help='刷流(0-否/1-是)', location='form') + parser.add_argument('signin', type=int, help='签到(0-否/1-是)', location='form') + parser.add_argument('statistic', type=int, help='数据统计(0-否/1-是)', location='form') + + def post(self): + """ + 查询站点列表 + """ + return WebAction().api_action(cmd='get_sites', data=self.parser.parse_args()) + + +@site.route('/indexers') +class SiteIndexers(ClientResource): + + @staticmethod + def post(): + """ + 查询站点索引列表 + """ + return WebAction().api_action(cmd='get_indexers') + + +@search.route('/keyword') +class SearchKeyword(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('search_word', type=str, help='搜索关键字', location='form', required=True) + parser.add_argument('unident', type=int, help='快速模式(0-否/1-是)', location='form') + parser.add_argument('filters', type=str, help='过滤条件', location='form') + parser.add_argument('tmdbid', type=str, help='TMDBID', location='form') + parser.add_argument('media_type', type=str, help='类型(电影/电视剧)', location='form') + + @search.doc(parser=parser) + def post(self): + """ + 根据关键字/TMDBID搜索 + """ + return WebAction().api_action(cmd='search', data=self.parser.parse_args()) + + +@search.route('/result') +class SearchResult(ClientResource): + @staticmethod + def post(): + """ + 查询搜索结果 + """ + return WebAction().api_action(cmd='get_search_result') + + +@download.route('/search') +class DownloadSearch(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='搜索结果ID', location='form', required=True) + parser.add_argument('dir', type=str, help='保存目录', location='form') + parser.add_argument('setting', type=str, help='下载设置', location='form') + + @download.doc(parser=parser) + def post(self): + """ + 下载搜索结果 + """ + return WebAction().api_action(cmd='download', data=self.parser.parse_args()) + + +@download.route('/item') +class DownloadItem(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('enclosure', type=str, help='链接URL', location='form', required=True) + parser.add_argument('title', type=str, help='标题', location='form', required=True) + parser.add_argument('site', type=str, help='站点名称', location='form') + parser.add_argument('description', type=str, help='描述', location='form') + parser.add_argument('page_url', type=str, help='详情页面URL', location='form') + parser.add_argument('size', type=str, help='大小', location='form') + parser.add_argument('seeders', type=str, help='做种数', location='form') + parser.add_argument('uploadvolumefactor', type=float, help='上传因子', location='form') + parser.add_argument('downloadvolumefactor', type=float, help='下载因子', location='form') + parser.add_argument('dl_dir', type=str, help='保存目录', location='form') + + @download.doc(parser=parser) + def post(self): + """ + 下载链接 + """ + return WebAction().api_action(cmd='download_link', data=self.parser.parse_args()) + + +@download.route('/start') +class DownloadStart(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='任务ID', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 开始下载任务 + """ + return WebAction().api_action(cmd='pt_start', data=self.parser.parse_args()) + + +@download.route('/stop') +class DownloadStop(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='任务ID', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 暂停下载任务 + """ + return WebAction().api_action(cmd='pt_stop', data=self.parser.parse_args()) + + +@download.route('/info') +class DownloadInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('ids', type=str, help='任务IDS', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 查询下载进度 + """ + return WebAction().api_action(cmd='pt_info', data=self.parser.parse_args()) + + +@download.route('/remove') +class DownloadRemove(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='任务ID', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 删除下载任务 + """ + return WebAction().api_action(cmd='pt_remove', data=self.parser.parse_args()) + + +@download.route('/history') +class DownloadHistory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('page', type=str, help='第几页', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 查询下载历史 + """ + return WebAction().api_action(cmd='get_downloaded', data=self.parser.parse_args()) + + +@download.route('/now') +class DownloadNow(ClientResource): + @staticmethod + def post(): + """ + 查询正在下载的任务 + """ + return WebAction().api_action(cmd='get_downloading') + + +@download.route('/config/info') +class DownloadConfigInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=str, help='下载设置ID', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 查询下载设置 + """ + return WebAction().api_action(cmd='get_download_setting', data=self.parser.parse_args()) + + +@download.route('/config/update') +class DownloadConfigUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=str, help='下载设置ID', location='form', required=True) + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('category', type=str, help='分类', location='form') + parser.add_argument('tags', type=str, help='标签', location='form') + parser.add_argument('content_layout', type=int, help='布局(0-全局/1-原始/2-创建子文件夹/3-不建子文件夹)', + location='form') + parser.add_argument('is_paused', type=int, help='动作(0-添加后开始/1-添加后暂停)', location='form') + parser.add_argument('upload_limit', type=int, help='上传速度限制', location='form') + parser.add_argument('download_limit', type=int, help='下载速度限制', location='form') + parser.add_argument('ratio_limit', type=int, help='分享率限制', location='form') + parser.add_argument('seeding_time_limit', type=int, help='做种时间限制', location='form') + parser.add_argument('downloader', type=str, help='下载器(Qbittorrent/Transmission)', location='form') + + @download.doc(parser=parser) + def post(self): + """ + 新增/修改下载设置 + """ + return WebAction().api_action(cmd='update_download_setting', data=self.parser.parse_args()) + + +@download.route('/config/delete') +class DownloadConfigDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=str, help='下载设置ID', location='form', required=True) + + @download.doc(parser=parser) + def post(self): + """ + 删除下载设置 + """ + return WebAction().api_action(cmd='delete_download_setting', data=self.parser.parse_args()) + + +@download.route('/config/list') +class DownloadConfigList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=str, help='ID', location='form') + + def post(self): + """ + 查询下载设置 + """ + return WebAction().api_action(cmd="get_download_setting", data=self.parser.parse_args()) + + +@download.route('/config/directory') +class DownloadConfigDirectory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=str, help='下载设置ID', location='form') + + def post(self): + """ + 查询下载保存目录 + """ + return WebAction().api_action(cmd="get_download_dirs", data=self.parser.parse_args()) + + +@organization.route('/unknown/delete') +class UnknownDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='未识别记录ID', location='form', required=True) + + @organization.doc(parser=parser) + def post(self): + """ + 删除未识别记录 + """ + return WebAction().api_action(cmd='del_unknown_path', data=self.parser.parse_args()) + + +@organization.route('/unknown/rename') +class UnknownRename(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('logid', type=str, help='转移历史记录ID', location='form') + parser.add_argument('unknown_id', type=str, help='未识别记录ID', location='form') + parser.add_argument('syncmod', type=str, help='转移模式', location='form', required=True) + parser.add_argument('tmdb', type=int, help='TMDB ID', location='form') + parser.add_argument('title', type=str, help='标题', location='form') + parser.add_argument('year', type=str, help='年份', location='form') + parser.add_argument('type', type=str, help='类型(MOV/TV/ANIME)', location='form') + parser.add_argument('season', type=int, help='季号', location='form') + parser.add_argument('episode_format', type=str, help='集数定位', location='form') + parser.add_argument('min_filesize', type=int, help='最小文件大小', location='form') + + @organization.doc(parser=parser) + def post(self): + """ + 手动识别 + """ + return WebAction().api_action(cmd='rename', data=self.parser.parse_args()) + + +@organization.route('/unknown/renameudf') +class UnknownRenameUDF(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('inpath', type=str, help='源目录', location='form', required=True) + parser.add_argument('outpath', type=str, help='目的目录', location='form', required=True) + parser.add_argument('syncmod', type=str, help='转移模式', location='form', required=True) + parser.add_argument('tmdb', type=int, help='TMDB ID', location='form') + parser.add_argument('title', type=str, help='标题', location='form') + parser.add_argument('year', type=str, help='年份', location='form') + parser.add_argument('type', type=str, help='类型(MOV/TV/ANIME)', location='form') + parser.add_argument('season', type=int, help='季号', location='form') + parser.add_argument('episode_format', type=str, help='集数定位', location='form') + parser.add_argument('episode_details', type=str, help='集数范围', location='form') + parser.add_argument('episode_offset', type=str, help='集数偏移', location='form') + parser.add_argument('min_filesize', type=int, help='最小文件大小', location='form') + + @organization.doc(parser=parser) + def post(self): + """ + 自定义识别 + """ + return WebAction().api_action(cmd='rename_udf', data=self.parser.parse_args()) + + +@organization.route('/unknown/redo') +class UnknownRedo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('flag', type=str, help='类型(unknow/history)', location='form', required=True) + parser.add_argument('ids', type=list, help='记录ID', location='form', required=True) + + @organization.doc(parser=parser) + def post(self): + """ + 重新识别 + """ + return WebAction().api_action(cmd='re_identification', data=self.parser.parse_args()) + + +@organization.route('/history/delete') +class TransferHistoryDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('logids', type=list, help='记录IDS', location='form', required=True) + + @organization.doc(parser=parser) + def post(self): + """ + 删除媒体整理历史记录 + """ + return WebAction().api_action(cmd='delete_history', data=self.parser.parse_args()) + + +@organization.route('/unknown/list') +class TransferUnknownList(ClientResource): + @staticmethod + def post(): + """ + 查询所有未识别记录 + """ + return WebAction().api_action(cmd='get_unknown_list') + + +@organization.route('/history/list') +class TransferHistoryList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('page', type=int, help='页码', location='form', required=True) + parser.add_argument('pagenum', type=int, help='每页条数', location='form', required=True) + parser.add_argument('keyword', type=str, help='过滤关键字', location='form') + + @organization.doc(parser=parser) + def post(self): + """ + 查询媒体整理历史记录 + """ + return WebAction().api_action(cmd='get_transfer_history', data=self.parser.parse_args()) + + +@organization.route('/history/statistics') +class HistoryStatistics(ClientResource): + + @staticmethod + def post(): + """ + 查询转移历史统计数据 + """ + return WebAction().api_action(cmd='get_transfer_statistics') + + +@organization.route('/cache/empty') +class TransferCacheEmpty(ClientResource): + + @staticmethod + def post(): + """ + 清空文件转移缓存 + """ + return WebAction().api_action(cmd='truncate_blacklist') + + +@library.route('/sync/start') +class LibrarySyncStart(ClientResource): + + @staticmethod + def post(): + """ + 开始媒体库同步 + """ + return WebAction().api_action(cmd='start_mediasync') + + +@library.route('/sync/status') +class LibrarySyncStatus(ClientResource): + + @staticmethod + def post(): + """ + 查询媒体库同步状态 + """ + return WebAction().api_action(cmd='mediasync_state') + + +@library.route('/mediaserver/playhistory') +class LibraryPlayHistory(ClientResource): + + @staticmethod + def post(): + """ + 查询媒体库播放历史 + """ + return WebAction().api_action(cmd='get_library_playhistory') + + +@library.route('/mediaserver/statistics') +class LibraryStatistics(ClientResource): + + @staticmethod + def post(): + """ + 查询媒体库统计数据 + """ + return WebAction().api_action(cmd="get_library_mediacount") + + +@library.route('/space') +class LibrarySpace(ClientResource): + + @staticmethod + def post(): + """ + 查询媒体库存储空间 + """ + return WebAction().api_action(cmd='get_library_spacesize') + + +@system.route('/logging') +class SystemLogging(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('refresh_new', type=int, help='是否刷新增量日志(0-否/1-是)', location='form', required=True) + + @system.doc(parser=parser) + def post(self): + """ + 获取实时日志 + """ + return WebAction().api_action(cmd='logging', data=self.parser.parse_args()) + + +@system.route('/version') +class SystemVersion(ClientResource): + + @staticmethod + def post(): + """ + 查询最新版本号 + """ + return WebAction().api_action(cmd='version') + + +@system.route('/path') +class SystemPath(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('dir', type=str, help='路径', location='form', required=True) + parser.add_argument('filter', type=str, help='过滤器(ONLYFILE/ONLYDIR/MEDIAFILE/SUBFILE/ALL)', location='form', + required=True) + + @system.doc(parser=parser) + def post(self): + """ + 查询目录的子目录/文件 + """ + return WebAction().api_action(cmd='get_sub_path', data=self.parser.parse_args()) + + +@system.route('/restart') +class SystemRestart(ClientResource): + + @staticmethod + def post(): + """ + 重启 + """ + return WebAction().api_action(cmd='restart') + + +@system.route('/update') +class SystemUpdate(ClientResource): + + @staticmethod + def post(): + """ + 升级 + """ + return WebAction().api_action(cmd='update_system') + + +@system.route('/logout') +class SystemUpdate(ClientResource): + + @staticmethod + def post(): + """ + 注销 + """ + token = request.headers.get("Authorization", default=None) + if token: + TokenCache.delete(token) + return { + "code": 0, + "success": True + } + + +@system.route('/message') +class SystemMessage(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('lst_time', type=str, help='时间(YYYY-MM-DD HH24:MI:SS)', location='form') + + @system.doc(parser=parser) + def post(self): + """ + 查询消息中心消息 + """ + return WebAction().get_system_message(lst_time=self.parser.parse_args().get("lst_time")) + + +@system.route('/progress') +class SystemProgress(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(search/mediasync)', location='form', required=True) + + @system.doc(parser=parser) + def post(self): + """ + 查询搜索/媒体同步等进度 + """ + return WebAction().api_action(cmd='refresh_process', data=self.parser.parse_args()) + + +@config.route('/update') +class ConfigUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('items', type=dict, help='配置项', location='form', required=True) + + @config.doc(parser=parser) + def post(self): + """ + 新增/修改配置 + """ + return WebAction().api_action(cmd='update_config', data=self.parser.parse_args().get("items")) + + +@config.route('/test') +class ConfigTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('command', type=str, help='测试命令', location='form', required=True) + + @config.doc(parser=parser) + def post(self): + """ + 测试配置连通性 + """ + return WebAction().api_action(cmd='test_connection', data=self.parser.parse_args()) + + +@config.route('/restore') +class ConfigRestore(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('file_name', type=str, help='备份文件名', location='form', required=True) + + @config.doc(parser=parser) + def post(self): + """ + 恢复备份的配置 + """ + return WebAction().api_action(cmd='restory_backup', data=self.parser.parse_args()) + + +@config.route('/info') +class ConfigInfo(ClientResource): + @staticmethod + def post(): + """ + 获取所有配置信息 + """ + return { + "code": 0, + "success": True, + "data": Config().get_config() + } + + +@config.route('/directory') +class ConfigDirectory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('oper', type=str, help='操作类型(add/sub/set)', location='form', required=True) + parser.add_argument('key', type=str, help='配置项', location='form', required=True) + parser.add_argument('value', type=str, help='配置值', location='form', required=True) + + @config.doc(parser=parser) + def post(self): + """ + 配置媒体库目录 + """ + return WebAction().api_action(cmd='update_directory', data=self.parser.parse_args()) + + +@subscribe.route('/delete') +class SubscribeDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='名称', location='form') + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form') + parser.add_argument('year', type=str, help='发行年份', location='form') + parser.add_argument('season', type=int, help='季号', location='form') + parser.add_argument('rssid', type=int, help='已有订阅ID', location='form') + parser.add_argument('tmdbid', type=str, help='TMDBID', location='form') + + @subscribe.doc(parser=parser) + def post(self): + """ + 删除订阅 + """ + return WebAction().api_action(cmd='remove_rss_media', data=self.parser.parse_args()) + + +@subscribe.route('/add') +class SubscribeAdd(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('year', type=str, help='发行年份', location='form') + parser.add_argument('keyword', type=str, help='自定义搜索词', location='form') + parser.add_argument('season', type=int, help='季号', location='form') + parser.add_argument('rssid', type=int, help='已有订阅ID', location='form') + parser.add_argument('mediaid', type=str, help='TMDBID/DB:豆瓣ID', location='form') + parser.add_argument('fuzzy_match', type=int, help='模糊匹配(0-否/1-是)', location='form') + parser.add_argument('rss_sites', type=list, help='RSS站点', location='form') + parser.add_argument('search_sites', type=list, help='搜索站点', location='form') + parser.add_argument('over_edition', type=int, help='洗版(0-否/1-是)', location='form') + parser.add_argument('filter_restype', type=str, help='资源类型', location='form') + parser.add_argument('filter_pix', type=str, help='分辨率', location='form') + parser.add_argument('filter_team', type=str, help='字幕组/发布组', location='form') + parser.add_argument('filter_rule', type=int, help='过滤规则', location='form') + parser.add_argument('download_setting', type=int, help='下载设置', location='form') + parser.add_argument('save_path', type=str, help='保存路径', location='form') + parser.add_argument('total_ep', type=int, help='总集数', location='form') + parser.add_argument('current_ep', type=int, help='开始集数', location='form') + + @subscribe.doc(parser=parser) + def post(self): + """ + 新增/修改订阅 + """ + return WebAction().api_action(cmd='add_rss_media', data=self.parser.parse_args()) + + +@subscribe.route('/movie/date') +class SubscribeMovieDate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='TMDBID/DB:豆瓣ID', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 电影上映日期 + """ + return WebAction().api_action(cmd='movie_calendar_data', data=self.parser.parse_args()) + + +@subscribe.route('/tv/date') +class SubscribeTVDate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='TMDBID/DB:豆瓣ID', location='form', required=True) + parser.add_argument('season', type=int, help='季号', location='form', required=True) + parser.add_argument('name', type=str, help='名称', location='form') + + @subscribe.doc(parser=parser) + def post(self): + """ + 电视剧上映日期 + """ + return WebAction().api_action(cmd='tv_calendar_data', data=self.parser.parse_args()) + + +@subscribe.route('/search') +class SubscribeSearch(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('rssid', type=int, help='订阅ID', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 订阅刷新搜索 + """ + return WebAction().api_action(cmd='refresh_rss', data=self.parser.parse_args()) + + +@subscribe.route('/info') +class SubscribeInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('rssid', type=int, help='订阅ID', location='form', required=True) + parser.add_argument('type', type=str, help='订阅类型(MOV/TV)', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 订阅详情 + """ + return WebAction().api_action(cmd='rss_detail', data=self.parser.parse_args()) + + +@subscribe.route('/redo') +class SubscribeRedo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('rssid', type=int, help='订阅历史ID', location='form', required=True) + parser.add_argument('type', type=str, help='订阅类型(MOV/TV)', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 历史重新订阅 + """ + return WebAction().api_action(cmd='re_rss_history', data=self.parser.parse_args()) + + +@subscribe.route('/history/delete') +class SubscribeHistoryDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('rssid', type=int, help='订阅ID', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 删除订阅历史 + """ + return WebAction().api_action(cmd='delete_rss_history', data=self.parser.parse_args()) + + +@subscribe.route('/history') +class SubscribeHistory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + + @subscribe.doc(parser=parser) + def post(self): + """ + 查询订阅历史 + """ + return WebAction().api_action(cmd='get_rss_history', data=self.parser.parse_args()) + + +@subscribe.route('/cache/delete') +class SubscribeCacheDelete(ClientResource): + @staticmethod + def post(): + """ + 清理订阅缓存 + """ + return WebAction().api_action(cmd='truncate_rsshistory') + + +@subscribe.route('/movie/list') +class SubscribeMovieList(ClientResource): + @staticmethod + def post(): + """ + 查询所有电影订阅 + """ + return WebAction().api_action(cmd='get_movie_rss_list') + + +@subscribe.route('/tv/list') +class SubscribeTvList(ClientResource): + @staticmethod + def post(): + """ + 查询所有电视剧订阅 + """ + return WebAction().api_action(cmd='get_tv_rss_list') + + +@recommend.route('/list') +class RecommendList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, + help='类型(hm/ht/nm/nt/dbom/dbhm/dbht/dbdh/dbnm/dbtop/dbzy/bangumi)', + location='form', required=True) + parser.add_argument('page', type=int, help='页码', location='form', required=True) + + @recommend.doc(parser=parser) + def post(self): + """ + 推荐列表 + """ + return WebAction().api_action(cmd='get_recommend', data=self.parser.parse_args()) + + +@rss.route('/info') +class RssInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='任务ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅任务详情 + """ + return WebAction().api_action(cmd='get_userrss_task', data=self.parser.parse_args()) + + +@rss.route('/delete') +class RssDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='任务ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 删除自定义订阅任务 + """ + return WebAction().api_action(cmd='delete_userrss_task', data=self.parser.parse_args()) + + +@rss.route('/update') +class RssUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='任务ID', location='form') + parser.add_argument('name', type=str, help='任务名称', location='form', required=True) + parser.add_argument('address', type=str, help='RSS地址', location='form', required=True) + parser.add_argument('parser', type=int, help='解析器ID', location='form', required=True) + parser.add_argument('interval', type=int, help='刷新间隔(分钟)', location='form', required=True) + parser.add_argument('uses', type=str, help='动作', location='form', required=True) + parser.add_argument('state', type=str, help='状态(Y/N)', location='form', required=True) + parser.add_argument('include', type=str, help='包含', location='form') + parser.add_argument('exclude', type=str, help='排除', location='form') + parser.add_argument('filterrule', type=int, help='过滤规则', location='form') + parser.add_argument('note', type=str, help='备注', location='form') + + @rss.doc(parser=parser) + def post(self): + """ + 新增/修改自定义订阅任务 + """ + return WebAction().api_action(cmd='update_userrss_task', data=self.parser.parse_args()) + + +@rss.route('/parser/info') +class RssParserInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='解析器ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 解析器详情 + """ + return WebAction().api_action(cmd='get_rssparser', data=self.parser.parse_args()) + + +@rss.route('/parser/delete') +class RssParserDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='解析器ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 删除解析器 + """ + return WebAction().api_action(cmd='delete_rssparser', data=self.parser.parse_args()) + + +@rss.route('/parser/update') +class RssParserUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='解析器ID', location='form', required=True) + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('type', type=str, help='类型(JSON/XML)', location='form', required=True) + parser.add_argument('format', type=str, help='解析格式', location='form', required=True) + parser.add_argument('params', type=str, help='附加参数', location='form') + + @rss.doc(parser=parser) + def post(self): + """ + 新增/修改解析器 + """ + return WebAction().api_action(cmd='update_rssparser', data=self.parser.parse_args()) + + +@rss.route('/parser/list') +class RssParserList(ClientResource): + @staticmethod + def post(): + """ + 查询所有解析器 + """ + return { + "code": 0, + "success": True, + "data": { + "parsers": RssChecker().get_userrss_parser() + } + } + + +@rss.route('/list') +class RssList(ClientResource): + @staticmethod + def post(): + """ + 查询所有自定义订阅任务 + """ + return { + "code": 0, + "success": False, + "data": { + "tasks": RssChecker().get_rsstask_info(), + "parsers": RssChecker().get_userrss_parser() + } + } + + +@rss.route('/preview') +class RssPreview(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='任务ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅预览 + """ + return WebAction().api_action(cmd='list_rss_articles', data=self.parser.parse_args()) + + +@rss.route('/name/test') +class RssNameTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('taskid', type=int, help='任务ID', location='form', required=True) + parser.add_argument('title', type=str, help='名称', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅名称测试 + """ + return WebAction().api_action(cmd='rss_article_test', data=self.parser.parse_args()) + + +@rss.route('/item/history') +class RssItemHistory(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='任务ID', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅任务条目处理记录 + """ + return WebAction().api_action(cmd='list_rss_history', data=self.parser.parse_args()) + + +@rss.route('/item/set') +class RssItemSet(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('flag', type=str, help='操作类型(set_finished/set_unfinish)', location='form', required=True) + parser.add_argument('articles', type=list, help='条目({title/enclosure})', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅任务条目状态调整 + """ + return WebAction().api_action(cmd='rss_articles_check', data=self.parser.parse_args()) + + +@rss.route('/item/download') +class RssItemDownload(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('taskid', type=int, help='任务ID', location='form', required=True) + parser.add_argument('articles', type=list, help='条目({title/enclosure})', location='form', required=True) + + @rss.doc(parser=parser) + def post(self): + """ + 自定义订阅任务条目下载 + """ + return WebAction().api_action(cmd='rss_articles_download', data=self.parser.parse_args()) + + +@media.route('/search') +class MediaSearch(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('keyword', type=str, help='关键字', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 搜索TMDB/豆瓣词条 + """ + return WebAction().api_action(cmd='search_media_infos', data=self.parser.parse_args()) + + +@media.route('/cache/update') +class MediaCacheUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('key', type=str, help='缓存Key值', location='form', required=True) + parser.add_argument('title', type=str, help='标题', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 修改TMDB缓存标题 + """ + return WebAction().api_action(cmd='modify_tmdb_cache', data=self.parser.parse_args()) + + +@media.route('/cache/delete') +class MediaCacheDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('cache_key', type=str, help='缓存Key值', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 删除TMDB缓存 + """ + return WebAction().api_action(cmd='delete_tmdb_cache', data=self.parser.parse_args()) + + +@media.route('/cache/clear') +class MediaCacheClear(ClientResource): + + @staticmethod + def post(): + """ + 清空TMDB缓存 + """ + return WebAction().api_action(cmd='clear_tmdb_cache') + + +@media.route('/tv/seasons') +class MediaTvSeasons(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('tmdbid', type=str, help='TMDBID', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 查询电视剧季列表 + """ + return WebAction().api_action(cmd='get_tvseason_list', data=self.parser.parse_args()) + + +@media.route('/category/list') +class MediaCategoryList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(电影/电视剧/动漫)', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 查询二级分类配置 + """ + return WebAction().api_action(cmd='get_categories', data=self.parser.parse_args()) + + +@media.route('/info') +class MediaInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('id', type=str, help='TMDBID/DB:豆瓣ID', location='form') + parser.add_argument('title', type=str, help='标题', location='form') + parser.add_argument('year', type=str, help='年份', location='form') + parser.add_argument('rssid', type=str, help='订阅ID', location='form') + + @media.doc(parser=parser) + def post(self): + """ + 识别媒体信息 + """ + return WebAction().api_action(cmd='media_info', data=self.parser.parse_args()) + + +@media.route('/detail') +class MediaDetail(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('tmdbid', type=str, help='TMDBID/DB:豆瓣ID', location='form') + + @media.doc(parser=parser) + def post(self): + """ + 查询TMDB媒体详情 + """ + return WebAction().api_action(cmd='media_detail', data=self.parser.parse_args()) + + +@media.route('/similar') +class MediaSimilar(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('tmdbid', type=str, help='TMDBID', location='form') + parser.add_argument('page', type=int, help='页码', location='form') + + @media.doc(parser=parser) + def post(self): + """ + 根据TMDBID查询类似媒体 + """ + return WebAction().api_action(cmd='media_similar', data=self.parser.parse_args()) + + +@media.route('/recommendations') +class MediaRecommendations(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('tmdbid', type=str, help='TMDBID', location='form') + parser.add_argument('page', type=int, help='页码', location='form') + + @media.doc(parser=parser) + def post(self): + """ + 根据TMDBID查询推荐媒体 + """ + return WebAction().api_action(cmd='media_recommendations', data=self.parser.parse_args()) + + +@media.route('/person') +class MediaPersonList(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(MOV/TV)', location='form', required=True) + parser.add_argument('personid', type=str, help='演员ID', location='form') + parser.add_argument('page', type=int, help='页码', location='form') + + @media.doc(parser=parser) + def post(self): + """ + 查询TMDB演员参演作品 + """ + return WebAction().api_action(cmd='person_medias', data=self.parser.parse_args()) + + +@media.route('/subtitle/download') +class MediaSubtitleDownload(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('path', type=str, help='文件路径(含文件名)', location='form', required=True) + parser.add_argument('name', type=str, help='名称(用于识别)', location='form', required=True) + + @media.doc(parser=parser) + def post(self): + """ + 下载单个文件字幕 + """ + return WebAction().api_action(cmd='download_subtitle', data=self.parser.parse_args()) + + +@brushtask.route('/update') +class BrushTaskUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('brushtask_id', type=str, help='刷流任务ID', location='form') + parser.add_argument('brushtask_name', type=str, help='任务名称', location='form', required=True) + parser.add_argument('brushtask_site', type=int, help='站点', location='form', required=True) + parser.add_argument('brushtask_interval', type=int, help='刷新间隔(分钟)', location='form', required=True) + parser.add_argument('brushtask_downloader', type=int, help='下载器', location='form', required=True) + parser.add_argument('brushtask_totalsize', type=int, help='保种体积(GB)', location='form', required=True) + parser.add_argument('brushtask_state', type=str, help='状态(Y/N)', location='form', required=True) + parser.add_argument('brushtask_transfer', type=str, help='转移到媒体库(Y/N)', location='form') + parser.add_argument('brushtask_sendmessage', type=str, help='消息推送(Y/N)', location='form') + parser.add_argument('brushtask_forceupload', type=str, help='强制做种(Y/N)', location='form') + parser.add_argument('brushtask_free', type=str, help='促销(FREE/2XFREE)', location='form') + parser.add_argument('brushtask_hr', type=str, help='Hit&Run(HR)', location='form') + parser.add_argument('brushtask_torrent_size', type=int, help='种子大小(GB)', location='form') + parser.add_argument('brushtask_include', type=str, help='包含', location='form') + parser.add_argument('brushtask_exclude', type=str, help='排除', location='form') + parser.add_argument('brushtask_dlcount', type=int, help='同时下载任务数', location='form') + parser.add_argument('brushtask_peercount', type=int, help='做种人数限制', location='form') + parser.add_argument('brushtask_seedtime', type=float, help='做种时间(小时)', location='form') + parser.add_argument('brushtask_seedratio', type=float, help='分享率', location='form') + parser.add_argument('brushtask_seedsize', type=int, help='上传量(GB)', location='form') + parser.add_argument('brushtask_dltime', type=float, help='下载耗时(小时)', location='form') + parser.add_argument('brushtask_avg_upspeed', type=int, help='平均上传速度(KB/S)', location='form') + parser.add_argument('brushtask_iatime', type=float, help='未活动时间(小时)', location='form') + parser.add_argument('brushtask_pubdate', type=int, help='发布时间(小时)', location='form') + parser.add_argument('brushtask_upspeed', type=int, help='上传限速(KB/S)', location='form') + parser.add_argument('brushtask_downspeed', type=int, help='下载限速(KB/S)', location='form') + + @brushtask.doc(parser=parser) + def post(self): + """ + 新增/修改刷流任务 + """ + return WebAction().api_action(cmd='add_brushtask', data=self.parser.parse_args()) + + +@brushtask.route('/delete') +class BrushTaskDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='刷流任务ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 删除刷流任务 + """ + return WebAction().api_action(cmd='del_brushtask', data=self.parser.parse_args()) + + +@brushtask.route('/info') +class BrushTaskInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='刷流任务ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 刷流任务详情 + """ + return WebAction().api_action(cmd='brushtask_detail', data=self.parser.parse_args()) + + +@brushtask.route('/list') +class BrushTaskList(ClientResource): + @staticmethod + def post(): + """ + 查询所有刷流任务 + """ + return { + "code": 0, + "success": True, + "data": { + "tasks": BrushTask().get_brushtask_info() + } + } + + +@brushtask.route('/torrents') +class BrushTaskTorrents(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='刷流任务ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 查询刷流任务种子明细 + """ + return WebAction().api_action(cmd='list_brushtask_torrents', data=self.parser.parse_args()) + + +@brushtask.route('/downloader/update') +class BrushTaskDownloaderUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('test', type=int, help='测试(0-否/1-是)', location='form', required=True) + parser.add_argument('id', type=int, help='下载器ID', location='form') + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('type', type=str, help='类型(qbittorrent/transmission)', location='form', required=True) + parser.add_argument('host', type=str, help='地址', location='form', required=True) + parser.add_argument('port', type=int, help='端口', location='form', required=True) + parser.add_argument('username', type=str, help='用户名', location='form') + parser.add_argument('password', type=str, help='密码', location='form') + parser.add_argument('save_dir', type=str, help='保存目录', location='form') + + @brushtask.doc(parser=parser) + def post(self): + """ + 新增/修改刷流下载器 + """ + return WebAction().api_action(cmd='add_downloader', data=self.parser.parse_args()) + + +@brushtask.route('/downloader/delete') +class BrushTaskDownloaderDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='下载器ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 删除刷流下载器 + """ + return WebAction().api_action(cmd='delete_downloader', data=self.parser.parse_args()) + + +@brushtask.route('/downloader/info') +class BrushTaskDownloaderInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='下载器ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 刷流下载器详情 + """ + return WebAction().api_action(cmd='get_downloader', data=self.parser.parse_args()) + + +@brushtask.route('/downloader/list') +class BrushTaskDownloaderList(ClientResource): + @staticmethod + def post(): + """ + 查询所有刷流下载器 + """ + return { + "code": 0, + "success": True, + "data": { + "downloaders": BrushTask().get_downloader_info() + } + } + + +@brushtask.route('/run') +class BrushTaskRun(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='刷流任务ID', location='form', required=True) + + @brushtask.doc(parser=parser) + def post(self): + """ + 刷流下载器详情 + """ + return WebAction().api_action(cmd='run_brushtask', data=self.parser.parse_args()) + + +@filterrule.route('/list') +class FilterRuleList(ClientResource): + @staticmethod + def post(): + """ + 查询所有过滤规则 + """ + return WebAction().api_action(cmd='get_filterrules') + + +@filterrule.route('/group/add') +class FilterRuleGroupAdd(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('default', type=str, help='默认(Y/N)', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 新增规则组 + """ + return WebAction().api_action(cmd='add_filtergroup', data=self.parser.parse_args()) + + +@filterrule.route('/group/restore') +class FilterRuleGroupRestore(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('groupids', type=list, help='规则组ID', location='form', required=True) + parser.add_argument('init_rulegroups', type=list, help='规则组脚本', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 恢复默认规则组 + """ + return WebAction().api_action(cmd='restore_filtergroup', data=self.parser.parse_args()) + + +@filterrule.route('/group/default') +class FilterRuleGroupDefault(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='规则组ID', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 设置默认规则组 + """ + return WebAction().api_action(cmd='set_default_filtergroup', data=self.parser.parse_args()) + + +@filterrule.route('/group/delete') +class FilterRuleGroupDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=str, help='规则组ID', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 删除规则组 + """ + return WebAction().api_action(cmd='del_filtergroup', data=self.parser.parse_args()) + + +@filterrule.route('/rule/update') +class FilterRuleUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('rule_id', type=int, help='规则ID', location='form') + parser.add_argument('group_id', type=int, help='规则组ID', location='form', required=True) + parser.add_argument('rule_name', type=str, help='规则名称', location='form', required=True) + parser.add_argument('rule_pri', type=str, help='优先级', location='form', required=True) + parser.add_argument('rule_include', type=str, help='包含', location='form') + parser.add_argument('rule_exclude', type=str, help='排除', location='form') + parser.add_argument('rule_sizelimit', type=str, help='大小限制', location='form') + parser.add_argument('rule_free', type=str, help='促销(FREE/2XFREE)', location='form') + + @filterrule.doc(parser=parser) + def post(self): + """ + 新增/修改规则 + """ + return WebAction().api_action(cmd='add_filterrule', data=self.parser.parse_args()) + + +@filterrule.route('/rule/delete') +class FilterRuleDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='规则ID', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 删除规则 + """ + return WebAction().api_action(cmd='del_filterrule', data=self.parser.parse_args()) + + +@filterrule.route('/rule/info') +class FilterRuleInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('ruleid', type=int, help='规则ID', location='form', required=True) + parser.add_argument('groupid', type=int, help='规则组ID', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 规则详情 + """ + return WebAction().api_action(cmd='filterrule_detail', data=self.parser.parse_args()) + + +@filterrule.route('/rule/share') +class FilterRuleShare(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='规则组ID', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 分享规则组 + """ + return WebAction().api_action(cmd='share_filtergroup', data=self.parser.parse_args()) + + +@filterrule.route('/rule/import') +class FilterRuleImport(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('content', type=str, help='规则内容', location='form', required=True) + + @filterrule.doc(parser=parser) + def post(self): + """ + 导入规则组 + """ + return WebAction().api_action(cmd='import_filtergroup', data=self.parser.parse_args()) + + +@words.route('/group/add') +class WordsGroupAdd(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('tmdb_id', type=str, help='TMDBID', location='form', required=True) + parser.add_argument('tmdb_type', type=str, help='类型(movie/tv)', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 新增识别词组 + """ + return WebAction().api_action(cmd='add_custom_word_group', data=self.parser.parse_args()) + + +@words.route('/group/delete') +class WordsGroupDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('gid', type=int, help='识别词组ID', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 删除识别词组 + """ + return WebAction().api_action(cmd='delete_custom_word_group', data=self.parser.parse_args()) + + +@words.route('/item/update') +class WordItemUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='识别词ID', location='form', required=True) + parser.add_argument('gid', type=int, help='识别词组ID', location='form', required=True) + parser.add_argument('group_type', type=str, help='媒体类型(1-电影/2-电视剧)', location='form', required=True) + parser.add_argument('new_replaced', type=str, help='被替换词', location='form') + parser.add_argument('new_replace', type=str, help='替换词', location='form') + parser.add_argument('new_front', type=str, help='前定位词', location='form') + parser.add_argument('new_back', type=str, help='后定位词', location='form') + parser.add_argument('new_offset', type=str, help='偏移集数', location='form') + parser.add_argument('new_help', type=str, help='备注', location='form') + parser.add_argument('type', type=str, help='识别词类型(1-屏蔽/2-替换/3-替换+集偏移/4-集偏移)', location='form', + required=True) + parser.add_argument('season', type=str, help='季', location='form') + parser.add_argument('enabled', type=str, help='状态(1-启用/0-停用)', location='form', required=True) + parser.add_argument('regex', type=str, help='正则表达式(1-使用/0-不使用)', location='form') + + @words.doc(parser=parser) + def post(self): + """ + 新增/修改识别词 + """ + return WebAction().api_action(cmd='add_or_edit_custom_word', data=self.parser.parse_args()) + + +@words.route('/item/info') +class WordItemInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('wid', type=int, help='识别词ID', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 识别词详情 + """ + return WebAction().api_action(cmd='get_custom_word', data=self.parser.parse_args()) + + +@words.route('/item/delete') +class WordItemDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='识别词ID', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 删除识别词 + """ + return WebAction().api_action(cmd='delete_custom_word', data=self.parser.parse_args()) + + +@words.route('/item/status') +class WordItemStatus(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('ids_info', type=list, help='识别词IDS', location='form', required=True) + parser.add_argument('flag', type=int, help='状态(1/0)', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 设置识别词状态 + """ + return WebAction().api_action(cmd='check_custom_words', data=self.parser.parse_args()) + + +@words.route('/item/export') +class WordItemExport(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('note', type=str, help='备注', location='form', required=True) + parser.add_argument('ids_info', type=str, help='识别词IDS(@_)', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 导出识别词 + """ + return WebAction().api_action(cmd='export_custom_words', data=self.parser.parse_args()) + + +@words.route('/item/analyse') +class WordItemAnalyse(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('import_code', type=str, help='识别词代码', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 分析识别词 + """ + return WebAction().api_action(cmd='analyse_import_custom_words_code', data=self.parser.parse_args()) + + +@words.route('/item/import') +class WordItemImport(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('import_code', type=str, help='识别词代码', location='form', required=True) + parser.add_argument('ids_info', type=list, help='识别词IDS', location='form', required=True) + + @words.doc(parser=parser) + def post(self): + """ + 导入识别词 + """ + return WebAction().api_action(cmd='import_custom_words', data=self.parser.parse_args()) + + +@words.route('/list') +class WordList(ClientResource): + @staticmethod + def post(): + """ + 查询所有自定义识别词 + """ + return WebAction().api_action(cmd='get_customwords') + + +@sync.route('/directory/update') +class SyncDirectoryUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=int, help='同步目录ID', location='form') + parser.add_argument('from', type=str, help='源目录', location='form', required=True) + parser.add_argument('to', type=str, help='目的目录', location='form') + parser.add_argument('unknown', type=str, help='未知目录', location='form') + parser.add_argument('syncmod', type=str, help='同步模式', location='form') + parser.add_argument('rename', type=str, help='重命名', location='form') + parser.add_argument('enabled', type=str, help='开启', location='form') + + @sync.doc(parser=parser) + def post(self): + """ + 新增/修改同步目录 + """ + return WebAction().api_action(cmd='add_or_edit_sync_path', data=self.parser.parse_args()) + + +@sync.route('/directory/info') +class SyncDirectoryInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=int, help='同步目录ID', location='form', required=True) + + @sync.doc(parser=parser) + def post(self): + """ + 同步目录详情 + """ + return WebAction().api_action(cmd='get_sync_path', data=self.parser.parse_args()) + + +@sync.route('/directory/delete') +class SyncDirectoryDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=int, help='同步目录ID', location='form', required=True) + + @sync.doc(parser=parser) + def post(self): + """ + 删除同步目录 + """ + return WebAction().api_action(cmd='delete_sync_path', data=self.parser.parse_args()) + + +@sync.route('/directory/status') +class SyncDirectoryStatus(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=int, help='同步目录ID', location='form', required=True) + parser.add_argument('flag', type=str, help='操作(rename/enable)', location='form', required=True) + parser.add_argument('checked', type=int, help='状态(0-否/1-是)', location='form', required=True) + + @sync.doc(parser=parser) + def post(self): + """ + 设置同步目录状态 + """ + return WebAction().api_action(cmd='check_sync_path', data=self.parser.parse_args()) + + +@sync.route('/directory/list') +class SyncDirectoryList(ClientResource): + @staticmethod + def post(): + """ + 查询所有同步目录 + """ + return WebAction().api_action(cmd='get_directorysync') + + +@sync.route('/directory/run') +class SyncDirectoryRun(ApiResource): + parser = reqparse.RequestParser() + parser.add_argument('sid', type=int, help='同步目录ID', location='args', required=True) + + @sync.doc(parser=parser) + def get(self): + """ + 立即运行单个目录同步服务(密钥认证) + """ + return WebAction().api_action(cmd='run_directory_sync', data=self.parser.parse_args()) + + +@sync.route('/run') +class SyncRun(ApiResource): + + @staticmethod + def get(): + """ + 立即运行所有目录同步服务(密钥认证) + """ + return WebAction().api_action(cmd='sch', data={"item": "sync"}) + + +@message.route('/client/update') +class MessageClientUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('cid', type=int, help='ID', location='form') + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('type', type=str, help='类型(wechat/telegram/serverchan/bark/pushplus/iyuu/slack/gotify)', + location='form', required=True) + parser.add_argument('config', type=str, help='配置项(JSON)', location='form', required=True) + parser.add_argument('switchs', type=list, help='开关', location='form', required=True) + parser.add_argument('interactive', type=int, help='是否开启交互(0/1)', location='form', required=True) + parser.add_argument('enabled', type=int, help='是否启用(0/1)', location='form', required=True) + + @message.doc(parser=parser) + def post(self): + """ + 新增/修改通知消息服务渠道 + """ + return WebAction().api_action(cmd='update_message_client', data=self.parser.parse_args()) + + +@message.route('/client/delete') +class MessageClientDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('cid', type=int, help='ID', location='form', required=True) + + @message.doc(parser=parser) + def post(self): + """ + 删除通知消息服务渠道 + """ + return WebAction().api_action(cmd='delete_message_client', data=self.parser.parse_args()) + + +@message.route('/client/status') +class MessageClientStatus(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('flag', type=str, help='操作类型(interactive/enable)', location='form', required=True) + parser.add_argument('cid', type=int, help='ID', location='form', required=True) + + @message.doc(parser=parser) + def post(self): + """ + 设置通知消息服务渠道状态 + """ + return WebAction().api_action(cmd='check_message_client', data=self.parser.parse_args()) + + +@message.route('/client/info') +class MessageClientInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('cid', type=int, help='ID', location='form', required=True) + + @message.doc(parser=parser) + def post(self): + """ + 查询通知消息服务渠道设置 + """ + return WebAction().api_action(cmd='get_message_client', data=self.parser.parse_args()) + + +@message.route('/client/test') +class MessageClientTest(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('type', type=str, help='类型(wechat/telegram/serverchan/bark/pushplus/iyuu/slack/gotify)', + location='form', required=True) + parser.add_argument('config', type=str, help='配置(JSON)', location='form', required=True) + + @message.doc(parser=parser) + def post(self): + """ + 测试通知消息服务配置正确性 + """ + return WebAction().api_action(cmd='test_message_client', data=self.parser.parse_args()) + + +@torrentremover.route('/task/info') +class TorrentRemoverTaskInfo(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('tid', type=int, help='任务ID', location='form', required=True) + + @torrentremover.doc(parser=parser) + def post(self): + """ + 查询自动删种任务详情 + """ + return WebAction().api_action(cmd='get_torrent_remove_task', data=self.parser.parse_args()) + + +@torrentremover.route('/task/list') +class TorrentRemoverTaskList(ClientResource): + @staticmethod + @torrentremover.doc() + def post(): + """ + 查询所有自动删种任务 + """ + return WebAction().api_action(cmd='get_torrent_remove_task') + + +@torrentremover.route('/task/delete') +class TorrentRemoverTaskDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('tid', type=int, help='任务ID', location='form', required=True) + + @torrentremover.doc(parser=parser) + def post(self): + """ + 删除自动删种任务 + """ + return WebAction().api_action(cmd='delete_torrent_remove_task', data=self.parser.parse_args()) + + +@torrentremover.route('/task/update') +class TorrentRemoverTaskUpdate(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('tid', type=int, help='任务ID', location='form') + parser.add_argument('name', type=str, help='名称', location='form', required=True) + parser.add_argument('action', type=int, help='动作(1-暂停/2-删除种子/3-删除种子及文件)', location='form', + required=True) + parser.add_argument('interval', type=int, help='运行间隔(分钟)', location='form', required=True) + parser.add_argument('enabled', type=int, help='状态(0-停用/1-启用)', location='form', required=True) + parser.add_argument('samedata', type=int, help='处理辅种(0-否/1-是)', location='form', required=True) + parser.add_argument('onlynastool', type=int, help='只管理NASTool添加的下载(0-否/1-是)', location='form', + required=True) + parser.add_argument('ratio', type=float, help='分享率', location='form') + parser.add_argument('seeding_time', type=int, help='做种时间(小时)', location='form') + parser.add_argument('upload_avs', type=int, help='平均上传速度(KB/S)', location='form') + parser.add_argument('size', type=str, help='种子大小(GB)', location='form') + parser.add_argument('savepath_key', type=str, help='保存路径关键词', location='form') + parser.add_argument('tracker_key', type=str, help='tracker关键词', location='form') + parser.add_argument('downloader', type=str, help='下载器(Qb/Tr)', location='form') + parser.add_argument('qb_state', type=str, help='Qb种子状态(多个用;分隔)', location='form') + parser.add_argument('qb_category', type=str, help='Qb分类(多个用;分隔)', location='form') + parser.add_argument('tr_state', type=str, help='Tr种子状态(多个用;分隔)', location='form') + parser.add_argument('tr_error_key', type=str, help='Tr错误信息关键词', location='form') + + @torrentremover.doc(parser=parser) + def post(self): + """ + 新增/修改自动删种任务 + """ + return WebAction().api_action(cmd='update_torrent_remove_task', data=self.parser.parse_args()) + + +@douban.route('/history/list') +class DoubanHistoryList(ClientResource): + + @staticmethod + def post(): + """ + 查询豆瓣同步历史记录 + """ + return WebAction().api_action(cmd='get_douban_history') + + +@douban.route('/history/delete') +class DoubanHistoryDelete(ClientResource): + parser = reqparse.RequestParser() + parser.add_argument('id', type=int, help='ID', location='form', required=True) + + @douban.doc(parser=parser) + def post(self): + """ + 删除豆瓣同步历史记录 + """ + return WebAction().api_action(cmd='delete_douban_history', data=self.parser.parse_args()) + + +@douban.route('/run') +class DoubanRun(ClientResource): + @staticmethod + def post(): + """ + 立即同步豆瓣数据 + """ + # 返回站点信息 + return WebAction().api_action(cmd='sch', data={"item": "douban"}) diff --git a/web/backend/WXBizMsgCrypt3.py b/web/backend/WXBizMsgCrypt3.py new file mode 100644 index 0000000..ce10d0c --- /dev/null +++ b/web/backend/WXBizMsgCrypt3.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# -*- encoding:utf-8 -*- + +""" 对企业微信发送给企业后台的消息加解密示例代码. +@copyright: Copyright (c) 1998-2014 Tencent Inc. + +""" +import base64 +import hashlib +# ------------------------------------------------------------------------ +import logging +import random +import socket +import struct +import time +import xml.etree.cElementTree as ET + +from Crypto.Cipher import AES + +# Description:定义错误码含义 +######################################################################### +WXBizMsgCrypt_OK = 0 +WXBizMsgCrypt_ValidateSignature_Error = -40001 +WXBizMsgCrypt_ParseXml_Error = -40002 +WXBizMsgCrypt_ComputeSignature_Error = -40003 +WXBizMsgCrypt_IllegalAesKey = -40004 +WXBizMsgCrypt_ValidateCorpid_Error = -40005 +WXBizMsgCrypt_EncryptAES_Error = -40006 +WXBizMsgCrypt_DecryptAES_Error = -40007 +WXBizMsgCrypt_IllegalBuffer = -40008 +WXBizMsgCrypt_EncodeBase64_Error = -40009 +WXBizMsgCrypt_DecodeBase64_Error = -40010 +WXBizMsgCrypt_GenReturnXml_Error = -40011 + +""" +关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 +请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 +下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 +""" + + +class FormatException(Exception): + pass + + +def throw_exception(message, exception_class=FormatException): + """my define raise exception function""" + raise exception_class(message) + + +class SHA1: + """计算企业微信的消息签名接口""" + + @staticmethod + def getSHA1(token, timestamp, nonce, encrypt): + """用SHA1算法生成安全签名 + @param token: 票据 + @param timestamp: 时间戳 + @param encrypt: 密文 + @param nonce: 随机字符串 + @return: 安全签名 + """ + try: + sortlist = [token, timestamp, nonce, encrypt] + sortlist.sort() + sha = hashlib.sha1() + sha.update("".join(sortlist).encode()) + return WXBizMsgCrypt_OK, sha.hexdigest() + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return WXBizMsgCrypt_ComputeSignature_Error, None + + +class XMLParse: + """提供提取消息格式中的密文及生成回复消息格式的接口""" + + # xml消息模板 + AES_TEXT_RESPONSE_TEMPLATE = """ + + +%(timestamp)s + +""" + + @staticmethod + def extract(xmltext): + """提取出xml数据包中的加密消息 + @param xmltext: 待提取的xml字符串 + @return: 提取出的加密消息字符串 + """ + try: + xml_tree = ET.fromstring(xmltext) + encrypt = xml_tree.find("Encrypt") + return WXBizMsgCrypt_OK, encrypt.text + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return WXBizMsgCrypt_ParseXml_Error, None + + def generate(self, encrypt, signature, timestamp, nonce): + """生成xml消息 + @param encrypt: 加密后的消息密文 + @param signature: 安全签名 + @param timestamp: 时间戳 + @param nonce: 随机字符串 + @return: 生成的xml字符串 + """ + resp_dict = { + 'msg_encrypt': encrypt, + 'msg_signaturet': signature, + 'timestamp': timestamp, + 'nonce': nonce, + } + resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict + return resp_xml + + +class PKCS7Encoder: + """提供基于PKCS7算法的加解密接口""" + + block_size = 32 + + def encode(self, text): + """ 对需要加密的明文进行填充补位 + @param text: 需要进行填充补位操作的明文 + @return: 补齐明文字符串 + """ + text_length = len(text) + # 计算需要填充的位数 + amount_to_pad = self.block_size - (text_length % self.block_size) + if amount_to_pad == 0: + amount_to_pad = self.block_size + # 获得补位所用的字符 + pad = chr(amount_to_pad) + return text + (pad * amount_to_pad).encode() + + @staticmethod + def decode(decrypted): + """删除解密后明文的补位字符 + @param decrypted: 解密后的明文 + @return: 删除补位字符后的明文 + """ + pad = ord(decrypted[-1]) + if pad < 1 or pad > 32: + pad = 0 + return decrypted[:-pad] + + +class Prpcrypt(object): + """提供接收和推送给企业微信消息的加解密接口""" + + def __init__(self, key): + + # self.key = base64.b64decode(key+"=") + self.key = key + # 设置加解密模式为AES的CBC模式 + self.mode = AES.MODE_CBC + + def encrypt(self, text, receiveid): + """对明文进行加密 + @param text: 需要加密的明文 + @param receiveid: receiveid + @return: 加密得到的字符串 + """ + # 16位随机字符串添加到明文开头 + text = text.encode() + text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode() + + # 使用自定义的填充方式对明文进行补位填充 + pkcs7 = PKCS7Encoder() + text = pkcs7.encode(text) + # 加密 + cryptor = AES.new(self.key, self.mode, self.key[:16]) + try: + ciphertext = cryptor.encrypt(text) + # 使用BASE64对加密后的字符串进行编码 + return WXBizMsgCrypt_OK, base64.b64encode(ciphertext) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return WXBizMsgCrypt_EncryptAES_Error, None + + def decrypt(self, text, receiveid): + """对解密后的明文进行补位删除 + @param text: 密文 + @param receiveid: receiveid + @return: 删除填充补位后的明文 + """ + try: + cryptor = AES.new(self.key, self.mode, self.key[:16]) + # 使用BASE64对密文进行解码,然后AES-CBC解密 + plain_text = cryptor.decrypt(base64.b64decode(text)) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return WXBizMsgCrypt_DecryptAES_Error, None + try: + pad = plain_text[-1] + # 去掉补位字符串 + # pkcs7 = PKCS7Encoder() + # plain_text = pkcs7.encode(plain_text) + # 去除16位随机字符串 + content = plain_text[16:-pad] + xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) + xml_content = content[4: xml_len + 4] + from_receiveid = content[xml_len + 4:] + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return WXBizMsgCrypt_IllegalBuffer, None + + if from_receiveid.decode('utf8') != receiveid: + return WXBizMsgCrypt_ValidateCorpid_Error, None + return 0, xml_content + + @staticmethod + def get_random_str(): + """ 随机生成16位字符串 + @return: 16位字符串 + """ + return str(random.randint(1000000000000000, 9999999999999999)).encode() + + +class WXBizMsgCrypt(object): + # 构造函数 + def __init__(self, sToken, sEncodingAESKey, sReceiveId): + try: + self.key = base64.b64decode(sEncodingAESKey + "=") + assert len(self.key) == 32 + except Exception as err: + print(str(err)) + throw_exception("[error]: EncodingAESKey unvalid !", FormatException) + # return WXBizMsgCrypt_IllegalAesKey,None + self.m_sToken = sToken + self.m_sReceiveId = sReceiveId + + # 验证URL + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sEchoStr: 随机串,对应URL参数的echostr + # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + # @return:成功0,失败返回对应的错误码 + + def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) + return ret, sReplyEchoStr + + def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): + # 将企业回复用户的消息加密打包 + # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 + # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 + # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, + # return:成功0,sEncryptMsg,失败返回对应的错误码None + pc = Prpcrypt(self.key) + ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) + encrypt = encrypt.decode('utf8') + if ret != 0: + return ret, None + if timestamp is None: + timestamp = str(int(time.time())) + # 生成安全签名 + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) + if ret != 0: + return ret, None + xmlParse = XMLParse() + return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) + + def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): + # 检验消息的真实性,并且获取解密后的明文 + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sPostData: 密文,对应POST请求的数据 + # xml_content: 解密后的原文,当return返回0时有效 + # @return: 成功0,失败返回对应的错误码 + # 验证安全签名 + xmlParse = XMLParse() + ret, encrypt = xmlParse.extract(sPostData) + if ret != 0: + return ret, None + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) + return ret, xml_content diff --git a/web/backend/__init__.py b/web/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/search_torrents.py b/web/backend/search_torrents.py new file mode 100644 index 0000000..6fdfce6 --- /dev/null +++ b/web/backend/search_torrents.py @@ -0,0 +1,500 @@ +import os.path +import re + +import log +from app.downloader import Downloader +from app.helper import DbHelper, ProgressHelper +from app.indexer import Indexer +from app.media import Media, DouBan +from app.media.meta import MetaInfo +from app.message import Message +from app.searcher import Searcher +from app.sites import Sites +from app.subscribe import Subscribe +from app.utils import StringUtils, Torrent +from app.utils.types import SearchType, IndexerType +from config import Config +from web.backend.web_utils import WebUtils + +SEARCH_MEDIA_CACHE = {} +SEARCH_MEDIA_TYPE = {} + + +def search_medias_for_web(content, ident_flag=True, filters=None, tmdbid=None, media_type=None): + """ + WEB资源搜索 + :param content: 关键字文本,可以包括 类型、标题、季、集、年份等信息,使用 空格分隔,也支持种子的命名格式 + :param ident_flag: 是否进行媒体信息识别 + :param filters: 其它过滤条件 + :param tmdbid: TMDBID或DB:豆瓣ID + :param media_type: 媒体类型,配合tmdbid传入 + :return: 错误码,错误原因,成功时直接插入数据库 + """ + mtype, key_word, season_num, episode_num, year, content = StringUtils.get_keyword_from_string(content) + if not key_word: + log.info("【Web】%s 检索关键字有误!" % content) + return -1, "%s 未识别到搜索关键字!" % content + # 类型 + if media_type: + mtype = media_type + # 开始进度 + search_process = ProgressHelper() + search_process.start('search') + # 识别媒体 + media_info = None + if ident_flag: + + # 有TMDBID或豆瓣ID + if tmdbid: + media_info = WebUtils.get_mediainfo_from_id(mtype=mtype, mediaid=tmdbid) + else: + # 按输入名称查 + media_info = Media().get_media_info(mtype=media_type or mtype, + title=content) + + # 整合集 + if media_info: + if season_num: + media_info.begin_season = int(season_num) + if episode_num: + media_info.begin_episode = int(episode_num) + + if media_info and media_info.tmdb_info: + # 查询到TMDB信息 + log.info(f"【Web】从TMDB中匹配到{media_info.type.value}:{media_info.get_title_string()}") + # 查找的季 + if media_info.begin_season is None: + search_season = None + else: + search_season = media_info.get_season_list() + # 查找的集 + search_episode = media_info.get_episode_list() + if search_episode and not search_season: + search_season = [1] + # 中文名 + if media_info.cn_name: + search_cn_name = media_info.cn_name + else: + search_cn_name = media_info.title + # 英文名 + search_en_name = None + if media_info.en_name: + search_en_name = media_info.en_name + else: + if media_info.original_language == "en": + search_en_name = media_info.original_title + else: + en_title = Media().get_tmdb_en_title(media_info) + if en_title: + search_en_name = en_title + # 两次搜索名称 + second_search_name = None + if Config().get_config("laboratory").get("search_en_title"): + if search_en_name: + first_search_name = search_en_name + second_search_name = search_cn_name + else: + first_search_name = search_cn_name + else: + first_search_name = search_cn_name + if search_en_name: + second_search_name = search_en_name + + filter_args = {"season": search_season, + "episode": search_episode, + "year": media_info.year, + "type": media_info.type} + else: + # 查询不到数据,使用快速搜索 + log.info(f"【Web】{content} 未从TMDB匹配到媒体信息,将使用快速搜索...") + ident_flag = False + media_info = None + first_search_name = key_word + second_search_name = None + filter_args = { + "season": season_num, + "episode": episode_num, + "year": year + } + # 快速搜索 + else: + first_search_name = key_word + second_search_name = None + filter_args = { + "season": season_num, + "episode": episode_num, + "year": year + } + # 整合高级查询条件 + if filters: + filter_args.update(filters) + # 开始检索 + log.info("【Web】开始检索 %s ..." % content) + media_list = Searcher().search_medias(key_word=first_search_name, + filter_args=filter_args, + match_media=media_info, + in_from=SearchType.WEB) + # 使用第二名称重新搜索 + if ident_flag \ + and len(media_list) == 0 \ + and second_search_name \ + and second_search_name != first_search_name: + search_process.start('search') + search_process.update(ptype='search', + text="%s 未检索到资源,尝试通过 %s 重新检索 ..." % (first_search_name, second_search_name)) + log.info("【Searcher】%s 未检索到资源,尝试通过 %s 重新检索 ..." % (first_search_name, second_search_name)) + media_list = Searcher().search_medias(key_word=second_search_name, + filter_args=filter_args, + match_media=media_info, + in_from=SearchType.WEB) + # 清空缓存结果 + dbhepler = DbHelper() + dbhepler.delete_all_search_torrents() + # 结束进度 + search_process.end('search') + if len(media_list) == 0: + log.info("【Web】%s 未检索到任何资源" % content) + return 1, "%s 未检索到任何资源" % content + else: + log.info("【Web】共检索到 %s 个有效资源" % len(media_list)) + # 插入数据库 + media_list = sorted(media_list, key=lambda x: "%s%s%s" % (str(x.res_order).rjust(3, '0'), + str(x.site_order).rjust(3, '0'), + str(x.seeders).rjust(10, '0')), reverse=True) + dbhepler.insert_search_results(media_items=media_list, + ident_flag=ident_flag, + title=content) + return 0, "" + + +def search_media_by_message(input_str, in_from: SearchType, user_id, user_name=None): + """ + 输入字符串,解析要求并进行资源检索 + :param input_str: 输入字符串,可以包括标题、年份、季、集的信息,使用空格隔开 + :param in_from: 搜索下载的请求来源 + :param user_id: 需要发送消息的,传入该参数,则只给对应用户发送交互消息 + :param user_name: 用户名称 + :return: 请求的资源是否全部下载完整、请求的文本对应识别出来的媒体信息、请求的资源如果是剧集,则返回下载后仍然缺失的季集信息 + """ + global SEARCH_MEDIA_TYPE + global SEARCH_MEDIA_CACHE + + if not input_str: + log.info("【Searcher】检索关键字有误!") + return + # 如果是数字,表示选择项 + if input_str.isdigit() and int(input_str) < 10: + # 获取之前保存的可选项 + choose = int(input_str) - 1 + if not SEARCH_MEDIA_CACHE.get(user_id) or \ + choose < 0 or choose >= len(SEARCH_MEDIA_CACHE.get(user_id)): + Message().send_channel_msg(channel=in_from, + title="输入有误!", + user_id=user_id) + log.warn("【Web】错误的输入值:%s" % input_str) + return + media_info = SEARCH_MEDIA_CACHE[user_id][choose] + if not SEARCH_MEDIA_TYPE.get(user_id) \ + or SEARCH_MEDIA_TYPE.get(user_id) == "SEARCH": + # 如果是豆瓣数据,需要重新查询TMDB的数据 + if media_info.douban_id: + _title = media_info.get_title_string() + # 先从网页抓取(含TMDBID) + doubaninfo = DouBan().get_media_detail_from_web(media_info.douban_id) + if doubaninfo and doubaninfo.get("imdbid"): + tmdbid = Media().get_tmdbid_by_imdbid(doubaninfo.get("imdbid")) + if tmdbid: + # 按IMDBID查询TMDB + media_info.set_tmdb_info(Media().get_tmdb_info(mtype=media_info.type, tmdbid=tmdbid)) + media_info.imdb_id = doubaninfo.get("imdbid") + else: + search_episode = media_info.begin_episode + media_info = Media().get_media_info(title="%s %s" % (media_info.title, media_info.year), + mtype=media_info.type, + strict=True) + media_info.begin_episode = search_episode + if not media_info or not media_info.tmdb_info: + Message().send_channel_msg(channel=in_from, + title="%s 从TMDB查询不到媒体信息!" % _title, + user_id=user_id) + return + # 搜索 + __search_media(in_from=in_from, + media_info=media_info, + user_id=user_id, + user_name=user_name) + else: + # 订阅 + __rss_media(in_from=in_from, + media_info=media_info, + user_id=user_id, + user_name=user_name) + # 接收到文本,开始查询可能的媒体信息供选择 + else: + if input_str.startswith("订阅"): + SEARCH_MEDIA_TYPE[user_id] = "SUBSCRIBE" + input_str = re.sub(r"订阅[::\s]*", "", input_str) + elif input_str.startswith("http") or input_str.startswith("magnet:"): + SEARCH_MEDIA_TYPE[user_id] = "DOWNLOAD" + else: + input_str = re.sub(r"(搜索|下载)[::\s]*", "", input_str) + SEARCH_MEDIA_TYPE[user_id] = "SEARCH" + + # 下载链接 + if SEARCH_MEDIA_TYPE[user_id] == "DOWNLOAD": + if input_str.startswith("http"): + # 检查是不是有这个站点 + site_info = Sites().get_sites(siteurl=input_str) + # 偿试下载种子文件 + filepath, content, retmsg = Torrent().save_torrent_file( + url=input_str, + cookie=site_info.get("cookie"), + ua=site_info.get("ua"), + proxy=site_info.get("proxy") + ) + # 下载种子出错 + if not content and retmsg: + Message().send_channel_msg(channel=in_from, + title=retmsg, + user_id=user_id) + return + if isinstance(content, str): + # 磁力链 + title = Torrent().get_magnet_title(content) + if title: + meta_info = Media().get_media_info(title=title) + else: + meta_info = MetaInfo(title="磁力链接") + meta_info.org_string = content + meta_info.set_torrent_info( + enclosure=content, + download_volume_factor=0, + upload_volume_factor=1 + ) + else: + # 识别文件名 + filename = os.path.basename(filepath) + # 识别 + meta_info = Media().get_media_info(title=filename) + meta_info.set_torrent_info( + enclosure=input_str + ) + else: + # 磁力链 + filepath = None + title = Torrent().get_magnet_title(input_str) + if title: + meta_info = Media().get_media_info(title=title) + else: + meta_info = MetaInfo(title="磁力链接") + meta_info.org_string = input_str + meta_info.set_torrent_info( + enclosure=input_str, + download_volume_factor=0, + upload_volume_factor=1 + ) + # 开始下载 + meta_info.user_name = user_name + state, retmsg = Downloader().download(media_info=meta_info, + torrent_file=filepath) + if state: + Message().send_download_message(in_from=in_from, + can_item=meta_info) + else: + Message().send_channel_msg(channel=in_from, + title=f"添加下载失败,{retmsg}", + user_id=user_id) + + # 搜索或订阅 + else: + # 获取字符串中可能的RSS站点列表 + rss_sites, content = StringUtils.get_idlist_from_string(input_str, + [{ + "id": site.get("name"), + "name": site.get("name") + } for site in Sites().get_sites(rss=True)]) + + # 索引器类型 + indexer_type = Indexer().get_client_type() + indexers = Indexer().get_indexers() + + # 获取字符串中可能的搜索站点列表 + if indexer_type == IndexerType.BUILTIN: + content = input_str + search_sites, _ = StringUtils.get_idlist_from_string(input_str, [{ + "id": indexer.name, + "name": indexer.name + } for indexer in indexers]) + else: + search_sites, content = StringUtils.get_idlist_from_string(content, [{ + "id": indexer.name, + "name": indexer.name + } for indexer in indexers]) + + # 获取字符串中可能的下载设置 + download_setting, content = StringUtils.get_idlist_from_string(content, [{ + "id": dl.get("id"), + "name": dl.get("name") + } for dl in Downloader().get_download_setting().values()]) + if download_setting: + download_setting = download_setting[0] + + # 识别媒体信息,列出匹配到的所有媒体 + log.info("【Web】正在识别 %s 的媒体信息..." % content) + if not content: + Message().send_channel_msg(channel=in_from, + title="无法识别搜索内容!", + user_id=user_id) + return + + # 搜索名称 + medias = WebUtils.search_media_infos( + keyword=content + ) + if not medias: + # 查询不到媒体信息 + Message().send_channel_msg(channel=in_from, + title="%s 查询不到媒体信息!" % content, + user_id=user_id) + return + + # 保存识别信息到临时结果中,由于消息长度限制只取前8条 + SEARCH_MEDIA_CACHE[user_id] = [] + for meta_info in medias[:8]: + # 合并站点和下载设置信息 + meta_info.rss_sites = rss_sites + meta_info.search_sites = search_sites + meta_info.set_download_info(download_setting=download_setting) + SEARCH_MEDIA_CACHE[user_id].append(meta_info) + + if 1 == len(SEARCH_MEDIA_CACHE[user_id]): + # 只有一条数据,直接开始搜索 + media_info = SEARCH_MEDIA_CACHE[user_id][0] + if not SEARCH_MEDIA_TYPE.get(user_id) \ + or SEARCH_MEDIA_TYPE.get(user_id) == "SEARCH": + # 如果是豆瓣数据,需要重新查询TMDB的数据 + if media_info.douban_id: + _title = media_info.get_title_string() + media_info = Media().get_media_info(title="%s %s" % (media_info.title, media_info.year), + mtype=media_info.type, strict=True) + if not media_info or not media_info.tmdb_info: + Message().send_channel_msg(channel=in_from, + title="%s 从TMDB查询不到媒体信息!" % _title, + user_id=user_id) + return + # 发送消息 + Message().send_channel_msg(channel=in_from, + title=media_info.get_title_vote_string(), + text=media_info.get_overview_string(), + image=media_info.get_message_image(), + url=media_info.get_detail_url(), + user_id=user_id) + # 开始搜索 + __search_media(in_from=in_from, + media_info=media_info, + user_id=user_id, + user_name=user_name) + else: + # 添加订阅 + __rss_media(in_from=in_from, + media_info=media_info, + user_id=user_id, + user_name=user_name) + else: + # 发送消息通知选择 + Message().send_channel_list_msg(channel=in_from, + title="共找到%s条相关信息,请回复对应序号" % len( + SEARCH_MEDIA_CACHE[user_id]), + medias=SEARCH_MEDIA_CACHE[user_id], + user_id=user_id) + + +def __search_media(in_from, media_info, user_id, user_name=None): + """ + 开始搜索和发送消息 + """ + # 检查是否存在,电视剧返回不存在的集清单 + exist_flag, no_exists, messages = Downloader().check_exists_medias(meta_info=media_info) + if messages: + Message().send_channel_msg(channel=in_from, + title="\n".join(messages), + user_id=user_id) + # 已经存在 + if exist_flag: + return + + # 开始检索 + Message().send_channel_msg(channel=in_from, + title="开始检索 %s ..." % media_info.title, + user_id=user_id) + search_result, no_exists, search_count, download_count = Searcher().search_one_media(media_info=media_info, + in_from=in_from, + no_exists=no_exists, + sites=media_info.search_sites, + user_name=user_name) + # 没有搜索到数据 + if not search_count: + Message().send_channel_msg(channel=in_from, + title="%s 未搜索到任何资源" % media_info.title, + user_id=user_id) + else: + # 搜索到了但是没开自动下载 + if download_count is None: + Message().send_channel_msg(channel=in_from, + title="%s 共搜索到%s个资源,点击选择下载" % (media_info.title, search_count), + image=media_info.get_message_image(), + url="search", + user_id=user_id) + return + else: + # 搜索到了但是没下载到数据 + if download_count == 0: + Message().send_channel_msg(channel=in_from, + title="%s 共搜索到%s个结果,但没有下载到任何资源" % ( + media_info.title, search_count), + user_id=user_id) + # 没有下载完成,且打开了自动添加订阅 + if not search_result and Config().get_config('pt').get('search_no_result_rss'): + # 添加订阅 + __rss_media(in_from=in_from, + media_info=media_info, + user_id=user_id, + state='R', + user_name=user_name) + + +def __rss_media(in_from, media_info, user_id=None, state='D', user_name=None): + """ + 开始添加订阅和发送消息 + """ + # 添加订阅 + if media_info.douban_id: + code, msg, media_info = Subscribe().add_rss_subscribe(mtype=media_info.type, + name=media_info.title, + year=media_info.year, + season=media_info.begin_season, + mediaid=f"DB:{media_info.douban_id}", + state=state, + rss_sites=media_info.rss_sites, + search_sites=media_info.search_sites) + else: + code, msg, media_info = Subscribe().add_rss_subscribe(mtype=media_info.type, + name=media_info.title, + year=media_info.year, + season=media_info.begin_season, + mediaid=media_info.tmdb_id, + state=state, + rss_sites=media_info.rss_sites, + search_sites=media_info.search_sites) + if code == 0: + log.info("【Web】%s %s 已添加订阅" % (media_info.type.value, media_info.get_title_string())) + if in_from in Message().get_search_types(): + media_info.user_name = user_name + Message().send_rss_success_message(in_from=in_from, + media_info=media_info) + else: + if in_from in Message().get_search_types(): + log.info("【Web】%s 添加订阅失败:%s" % (media_info.title, msg)) + Message().send_channel_msg(channel=in_from, + title="%s 添加订阅失败:%s" % (media_info.title, msg), + user_id=user_id) diff --git a/web/backend/user.py b/web/backend/user.py new file mode 100644 index 0000000..9acd1c8 --- /dev/null +++ b/web/backend/user.py @@ -0,0 +1,69 @@ +from flask_login import UserMixin +from werkzeug.security import check_password_hash + +from app.helper import DbHelper +from config import Config + + +class User(UserMixin): + """ + 用户 + """ + dbhelper = None + admin_users = [] + + def __init__(self, user=None): + self.dbhelper = DbHelper() + if user: + self.id = user.get('id') + self.username = user.get('name') + self.password_hash = user.get('password') + self.pris = user.get('pris') + self.admin_users = [{ + "id": 0, + "name": Config().get_config('app').get('login_user'), + "password": Config().get_config('app').get('login_password')[6:], + "pris": "我的媒体库,资源搜索,探索,站点管理,订阅管理,下载管理,媒体整理,服务,系统设置" + }] + + def verify_password(self, password): + """ + 验证密码 + """ + if self.password_hash is None: + return False + return check_password_hash(self.password_hash, password) + + def get_id(self): + """ + 获取用户ID + """ + return self.id + + def get(self, user_id): + """ + 根据用户ID获取用户实体,为 login_user 方法提供支持 + """ + if user_id is None: + return None + for user in self.admin_users: + if user.get('id') == user_id: + return User(user) + for user in self.dbhelper.get_users(): + if not user: + continue + if user.ID == user_id: + return User({"id": user.ID, "name": user.NAME, "password": user.PASSWORD, "pris": user.PRIS}) + return None + + def get_user(self, user_name): + """ + 根据用户名获取用户对像 + """ + for user in self.admin_users: + if user.get("name") == user_name: + return User(user) + for user in self.dbhelper.get_users(): + if user.NAME == user_name: + return User({"id": user.ID, "name": user.NAME, "password": user.PASSWORD, "pris": user.PRIS}) + return None diff --git a/web/backend/wallpaper.py b/web/backend/wallpaper.py new file mode 100644 index 0000000..2a64b5c --- /dev/null +++ b/web/backend/wallpaper.py @@ -0,0 +1,49 @@ +import base64 +import datetime +from functools import lru_cache + +from app.media import Media +from app.utils import RequestUtils, ExceptionUtils +from config import Config + + +@lru_cache(maxsize=1) +def get_login_wallpaper(today=datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d')): + """ + 获取Base64编码的壁纸图片 + """ + wallpaper = Config().get_config('app').get('wallpaper') + tmdbkey = Config().get_config('app').get('rmt_tmdbkey') + if (not wallpaper or wallpaper == "themoviedb") and tmdbkey: + img_url = __get_themoviedb_wallpaper(today) + else: + img_url = __get_bing_wallpaper(today) + if img_url: + res = RequestUtils().get_res(img_url) + if res and res.status_code == 200: + return base64.b64encode(res.content).decode() + return "" + + +def __get_themoviedb_wallpaper(today): + """ + 获取TheMovieDb的随机背景图 + """ + return Media().get_random_discover_backdrop() + + +def __get_bing_wallpaper(today): + """ + 获取Bing每日壁纸 + """ + url = "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&today=%s" % today + try: + resp = RequestUtils(timeout=5).get_res(url) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return "" + if resp and resp.status_code == 200: + if resp.json(): + for image in resp.json().get('images') or []: + return f"https://cn.bing.com{image.get('url')}" + return "" diff --git a/web/backend/web_utils.py b/web/backend/web_utils.py new file mode 100644 index 0000000..f333b20 --- /dev/null +++ b/web/backend/web_utils.py @@ -0,0 +1,157 @@ +import cn2an + +from app.media import Media, Bangumi, DouBan +from app.media.meta import MetaInfo +from app.utils import StringUtils, ExceptionUtils, SystemUtils, RequestUtils +from app.utils.types import MediaType +from config import Config +from version import APP_VERSION + + +class WebUtils: + + @staticmethod + def get_location(ip): + """ + 根据IP址查询真实地址 + """ + url = 'https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?co=&resource_id=6006&t=1529895387942&ie=utf8' \ + '&oe=gbk&cb=op_aladdin_callback&format=json&tn=baidu&' \ + 'cb=jQuery110203920624944751099_1529894588086&_=1529894588088&query=%s' % ip + r = RequestUtils().get_res(url) + r.encoding = 'gbk' + html = r.text + try: + c1 = html.split('location":"')[1] + c2 = c1.split('","')[0] + return c2 + except Exception as err: + ExceptionUtils.exception_traceback(err) + return "" + + @staticmethod + def get_current_version(): + """ + 获取当前版本号 + """ + commit_id = SystemUtils.execute('git rev-parse HEAD') + if commit_id and len(commit_id) > 7: + commit_id = commit_id[:7] + return "%s %s" % (APP_VERSION, commit_id) + + @staticmethod + def get_latest_version(): + """ + 获取最新版本号 + """ + try: + version_res = RequestUtils(proxies=Config().get_proxies()).get_res( + "https://api.github.com/repos/NAStool/nas-tools/releases/latest") + commit_res = RequestUtils(proxies=Config().get_proxies()).get_res( + "https://api.github.com/repos/NAStool/nas-tools/commits/master") + if version_res and commit_res: + ver_json = version_res.json() + commit_json = commit_res.json() + version = f"{ver_json['tag_name']} {commit_json['sha'][:7]}" + url = ver_json["html_url"] + return version, url, True + except Exception as e: + ExceptionUtils.exception_traceback(e) + return None, None, False + + @staticmethod + def get_mediainfo_from_id(mtype, mediaid): + """ + 根据TMDB/豆瓣/BANGUMI获取媒体信息 + """ + if not mediaid: + return None + media_info = None + if str(mediaid).startswith("DB:"): + # 豆瓣 + doubanid = mediaid[3:] + info = DouBan().get_douban_detail(doubanid=doubanid, mtype=mtype) + if not info: + return None + title = info.get("title") + original_title = info.get("original_title") + year = info.get("year") + if original_title: + media_info = Media().get_media_info(title=f"{original_title} {year}", + mtype=mtype, + append_to_response="all") + if not media_info or not media_info.tmdb_info: + media_info = Media().get_media_info(title=f"{title} {year}", + mtype=mtype, + append_to_response="all") + media_info.douban_id = doubanid + elif str(mediaid).startswith("BG:"): + # BANGUMI + bangumiid = str(mediaid)[3:] + info = Bangumi().detail(bid=bangumiid) + if not info: + return None + title = info.get("name") + title_cn = info.get("name_cn") + year = info.get("date")[:4] if info.get("date") else "" + media_info = Media().get_media_info(title=f"{title} {year}", + mtype=MediaType.TV, + append_to_response="all") + if not media_info or not media_info.tmdb_info: + media_info = Media().get_media_info(title=f"{title_cn} {year}", + mtype=MediaType.TV, + append_to_response="all") + else: + # TMDB + info = Media().get_tmdb_info(tmdbid=mediaid, + mtype=mtype, + append_to_response="all") + if not info: + return None + media_info = MetaInfo(title=info.get("title") if mtype == MediaType.MOVIE else info.get("name")) + media_info.set_tmdb_info(info) + + return media_info + + @staticmethod + def search_media_infos(keyword, source=None, page=1): + """ + 搜索TMDB或豆瓣词条 + :param: keyword 关键字 + :param: source 渠道 tmdb/douban + :param: season 季号 + :param: episode 集号 + """ + if not keyword: + return [] + mtype, key_word, season_num, episode_num, _, content = StringUtils.get_keyword_from_string(keyword) + if source == "tmdb": + use_douban_titles = False + elif source == "douban": + use_douban_titles = True + else: + use_douban_titles = Config().get_config("laboratory").get("use_douban_titles") + if use_douban_titles: + medias = DouBan().search_douban_medias(keyword=key_word, + mtype=mtype, + season=season_num, + episode=episode_num, + page=page) + else: + meta_info = MetaInfo(title=content) + tmdbinfos = Media().get_tmdb_infos(title=meta_info.get_name(), + year=meta_info.year, + mtype=mtype, + page=page) + medias = [] + for tmdbinfo in tmdbinfos: + tmp_info = MetaInfo(title=keyword) + tmp_info.set_tmdb_info(tmdbinfo) + if meta_info.type != MediaType.MOVIE and tmp_info.type == MediaType.MOVIE: + continue + if tmp_info.begin_season: + tmp_info.title = "%s 第%s季" % (tmp_info.title, cn2an.an2cn(meta_info.begin_season, mode='low')) + if tmp_info.begin_episode: + tmp_info.title = "%s 第%s集" % (tmp_info.title, meta_info.begin_episode) + medias.append(tmp_info) + return medias diff --git a/web/main.py b/web/main.py new file mode 100644 index 0000000..ecf6e02 --- /dev/null +++ b/web/main.py @@ -0,0 +1,1760 @@ +import base64 +import datetime +import os.path +import re +import shutil +import sqlite3 +import time +import traceback +import urllib +import xml.dom.minidom +from functools import wraps +from math import floor +from pathlib import Path +from threading import Lock +from urllib import parse + +from flask import Flask, request, json, render_template, make_response, session, send_from_directory, send_file +from flask_compress import Compress +from flask_login import LoginManager, login_user, login_required, current_user + +import log +from app.brushtask import BrushTask +from app.conf import ModuleConf, SystemConfig +from app.downloader import Downloader +from app.filter import Filter +from app.helper import SecurityHelper, MetaHelper, ChromeHelper, ThreadHelper +from app.indexer import Indexer +from app.media.meta import MetaInfo +from app.mediaserver import WebhookEvent +from app.message import Message +from app.rsschecker import RssChecker +from app.sites import Sites, SiteUserInfo +from app.speedlimiter import SpeedLimiter +from app.subscribe import Subscribe +from app.sync import Sync +from app.torrentremover import TorrentRemover +from app.utils import DomUtils, SystemUtils, ExceptionUtils, StringUtils +from app.utils.types import * +from config import PT_TRANSFER_INTERVAL, Config +from web.action import WebAction +from web.apiv1 import apiv1_bp +from web.backend.WXBizMsgCrypt3 import WXBizMsgCrypt +from web.backend.user import User +from web.backend.wallpaper import get_login_wallpaper +from web.backend.web_utils import WebUtils +from web.security import require_auth + +# 配置文件锁 +ConfigLock = Lock() + +# Flask App +App = Flask(__name__) +App.config['JSON_AS_ASCII'] = False +App.secret_key = os.urandom(24) +App.permanent_session_lifetime = datetime.timedelta(days=30) + +# 启用压缩 +Compress(App) + +# 登录管理模块 +LoginManager = LoginManager() +LoginManager.login_view = "login" +LoginManager.init_app(App) + +# API注册 +App.register_blueprint(apiv1_bp, url_prefix="/api/v1") + + +@App.after_request +def add_header(r): + """ + 统一添加Http头,标用缓存,避免Flask多线程+Chrome内核会发生的静态资源加载出错的问题 + r.headers["Cache-Control"] = "no-cache, no-store, max-age=0" + r.headers["Pragma"] = "no-cache" + r.headers["Expires"] = "0" + """ + return r + + +# 定义获取登录用户的方法 +@LoginManager.user_loader +def load_user(user_id): + return User().get(user_id) + + +# 页面不存在 +@App.errorhandler(404) +def page_not_found(error): + return render_template("404.html", error=error), 404 + + +# 服务错误 +@App.errorhandler(500) +def page_server_error(error): + return render_template("500.html", error=error), 500 + + +def action_login_check(func): + """ + Action安全认证 + """ + + @wraps(func) + def login_check(*args, **kwargs): + if not current_user.is_authenticated: + return {"code": -1, "msg": "用户未登录"} + return func(*args, **kwargs) + + return login_check + + +# 主页面 +@App.route('/', methods=['GET', 'POST']) +def login(): + def redirect_to_navigation(userinfo): + """ + 跳转到导航页面 + """ + # 判断当前的运营环境 + SystemFlag = SystemUtils.get_system() + SyncMod = Config().get_config('pt').get('rmt_mode') + TMDBFlag = 1 if Config().get_config('app').get('rmt_tmdbkey') else 0 + if not SyncMod: + SyncMod = "link" + RmtModeDict = WebAction().get_rmt_modes() + RestypeDict = ModuleConf.TORRENT_SEARCH_PARAMS.get("restype") + PixDict = ModuleConf.TORRENT_SEARCH_PARAMS.get("pix") + SiteFavicons = Sites().get_site_favicon() + Indexers = Indexer().get_indexers() + SearchSource = "douban" if Config().get_config("laboratory").get("use_douban_titles") else "tmdb" + CustomScriptCfg = SystemConfig().get_system_config("CustomScript") + return render_template('navigation.html', + GoPage=GoPage, + UserName=userinfo.username, + UserPris=str(userinfo.pris).split(","), + SystemFlag=SystemFlag.value, + TMDBFlag=TMDBFlag, + AppVersion=WebUtils.get_current_version(), + RestypeDict=RestypeDict, + PixDict=PixDict, + SyncMod=SyncMod, + SiteFavicons=SiteFavicons, + RmtModeDict=RmtModeDict, + Indexers=Indexers, + SearchSource=SearchSource, + CustomScriptCfg=CustomScriptCfg) + + def redirect_to_login(errmsg=''): + """ + 跳转到登录页面 + """ + return render_template('login.html', + GoPage=GoPage, + LoginWallpaper=get_login_wallpaper(), + err_msg=errmsg) + + # 登录认证 + if request.method == 'GET': + GoPage = request.args.get("next") or "" + if GoPage.startswith('/'): + GoPage = GoPage[1:] + if current_user.is_authenticated: + userid = current_user.id + username = current_user.username + if userid is None or username is None: + return redirect_to_login() + else: + # 登录成功 + return redirect_to_navigation(User().get_user(username)) + else: + return redirect_to_login() + + else: + GoPage = request.form.get('next') or "" + if GoPage.startswith('/'): + GoPage = GoPage[1:] + username = request.form.get('username') + password = request.form.get('password') + remember = request.form.get('remember') + if not username: + return redirect_to_login('请输入用户名') + user_info = User().get_user(username) + if not user_info: + return redirect_to_login('用户名或密码错误') + # 校验密码 + if user_info.verify_password(password): + # 创建用户 Session + login_user(user_info) + session.permanent = True if remember else False + # 登录成功 + return redirect_to_navigation(user_info) + else: + return redirect_to_login('用户名或密码错误') + + +# 开始 +@App.route('/index', methods=['POST', 'GET']) +@login_required +def index(): + # 媒体服务器类型 + MSType = Config().get_config('media').get('media_server') + # 获取媒体数量 + MediaCounts = WebAction().get_library_mediacount() + if MediaCounts.get("code") == 0: + ServerSucess = True + else: + ServerSucess = False + + # 获得活动日志 + Activity = WebAction().get_library_playhistory().get("result") + + # 磁盘空间 + LibrarySpaces = WebAction().get_library_spacesize() + + # 转移历史统计 + TransferStatistics = WebAction().get_transfer_statistics() + + return render_template("index.html", + ServerSucess=ServerSucess, + MediaCount={'MovieCount': MediaCounts.get("Movie"), + 'SeriesCount': MediaCounts.get("Series"), + 'SongCount': MediaCounts.get("Music"), + "EpisodeCount": MediaCounts.get("Episodes")}, + Activitys=Activity, + UserCount=MediaCounts.get("User"), + FreeSpace=LibrarySpaces.get("FreeSpace"), + TotalSpace=LibrarySpaces.get("TotalSpace"), + UsedSapce=LibrarySpaces.get("UsedSapce"), + UsedPercent=LibrarySpaces.get("UsedPercent"), + MovieChartLabels=TransferStatistics.get("MovieChartLabels"), + TvChartLabels=TransferStatistics.get("TvChartLabels"), + MovieNums=TransferStatistics.get("MovieNums"), + TvNums=TransferStatistics.get("TvNums"), + AnimeNums=TransferStatistics.get("AnimeNums"), + MediaServerType=MSType + ) + + +# 资源搜索页面 +@App.route('/search', methods=['POST', 'GET']) +@login_required +def search(): + # 权限 + if current_user.is_authenticated: + username = current_user.username + pris = User().get_user(username).get("pris") + else: + pris = "" + # 结果 + res = WebAction().get_search_result() + SearchResults = res.get("result") + Count = res.get("total") + return render_template("search.html", + UserPris=str(pris).split(","), + Count=Count, + Results=SearchResults, + SiteDict=Indexer().get_indexer_hash_dict(), + UPCHAR=chr(8593)) + + +# 电影订阅页面 +@App.route('/movie_rss', methods=['POST', 'GET']) +@login_required +def movie_rss(): + RssItems = WebAction().get_movie_rss_list().get("result") + RuleGroups = {str(group["id"]): group["name"] for group in Filter().get_rule_groups()} + DownloadSettings = Downloader().get_download_setting() + return render_template("rss/movie_rss.html", + Count=len(RssItems), + RuleGroups=RuleGroups, + DownloadSettings=DownloadSettings, + Items=RssItems + ) + + +# 电视剧订阅页面 +@App.route('/tv_rss', methods=['POST', 'GET']) +@login_required +def tv_rss(): + RssItems = WebAction().get_tv_rss_list().get("result") + RuleGroups = {str(group["id"]): group["name"] for group in Filter().get_rule_groups()} + DownloadSettings = Downloader().get_download_setting() + return render_template("rss/tv_rss.html", + Count=len(RssItems), + RuleGroups=RuleGroups, + DownloadSettings=DownloadSettings, + Items=RssItems + ) + + +# 订阅历史页面 +@App.route('/rss_history', methods=['POST', 'GET']) +@login_required +def rss_history(): + mtype = request.args.get("t") + RssHistory = WebAction().get_rss_history({"type": mtype}).get("result") + return render_template("rss/rss_history.html", + Count=len(RssHistory), + Items=RssHistory, + Type=mtype + ) + + +# 订阅日历页面 +@App.route('/rss_calendar', methods=['POST', 'GET']) +@login_required +def rss_calendar(): + Today = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d') + # 电影订阅 + RssMovieItems = [ + { + "tmdbid": movie.get("tmdbid"), + "rssid": movie.get("id") + } for movie in Subscribe().get_subscribe_movies().values() if movie.get("tmdbid") + ] + # 电视剧订阅 + RssTvItems = [ + { + "id": tv.get("tmdbid"), + "rssid": tv.get("id"), + "season": int(str(tv.get('season')).replace("S", "")), + "name": tv.get("name"), + } for tv in Subscribe().get_subscribe_tvs().values() if tv.get('season') and tv.get("tmdbid") + ] + # 自定义订阅 + RssTvItems += RssChecker().get_userrss_mediainfos() + # 电视剧订阅去重 + Uniques = set() + UniqueTvItems = [] + for item in RssTvItems: + unique = f"{item.get('id')}_{item.get('season')}" + if unique not in Uniques: + Uniques.add(unique) + UniqueTvItems.append(item) + return render_template("rss/rss_calendar.html", + Today=Today, + RssMovieItems=RssMovieItems, + RssTvItems=UniqueTvItems) + + +# 站点维护页面 +@App.route('/site', methods=['POST', 'GET']) +@login_required +def sites(): + CfgSites = Sites().get_sites() + RuleGroups = {str(group["id"]): group["name"] for group in Filter().get_rule_groups()} + DownloadSettings = {did: attr["name"] for did, attr in Downloader().get_download_setting().items()} + ChromeOk = ChromeHelper().get_status() + CookieCloudCfg = SystemConfig().get_system_config('CookieCloud') + CookieUserInfoCfg = SystemConfig().get_system_config('CookieUserInfo') + return render_template("site/site.html", + Sites=CfgSites, + RuleGroups=RuleGroups, + DownloadSettings=DownloadSettings, + ChromeOk=ChromeOk, + CookieCloudCfg=CookieCloudCfg, + CookieUserInfoCfg=CookieUserInfoCfg) + + +# 站点列表页面 +@App.route('/sitelist', methods=['POST', 'GET']) +@login_required +def sitelist(): + IndexerSites = Indexer().get_builtin_indexers(check=False, public=False) + return render_template("site/sitelist.html", + Sites=IndexerSites, + Count=len(IndexerSites)) + + +# 站点资源页面 +@App.route('/resources', methods=['POST', 'GET']) +@login_required +def resources(): + site_id = request.args.get("site") + site_name = request.args.get("title") + page = request.args.get("page") or 0 + keyword = request.args.get("keyword") + Results = WebAction().action("list_site_resources", {"id": site_id, "page": page, "keyword": keyword}).get( + "data") or [] + return render_template("site/resources.html", + Results=Results, + SiteId=site_id, + Title=site_name, + KeyWord=keyword, + TotalCount=len(Results), + PageRange=range(0, 10), + CurrentPage=int(page), + TotalPage=10) + + +# 推荐页面 +@App.route('/recommend', methods=['POST', 'GET']) +@login_required +def recommend(): + Type = request.args.get("type") or "" + SubType = request.args.get("subtype") or "" + Title = request.args.get("title") or "" + SubTitle = request.args.get("subtitle") or "" + CurrentPage = request.args.get("page") or 1 + Week = request.args.get("week") or "" + TmdbId = request.args.get("tmdbid") or "" + PersonId = request.args.get("personid") or "" + Keyword = request.args.get("keyword") or "" + Source = request.args.get("source") or "" + FilterKey = request.args.get("filter") or "" + Params = json.loads(request.args.get("params")) if request.args.get("params") else {} + return render_template("discovery/recommend.html", + Type=Type, + SubType=SubType, + Title=Title, + CurrentPage=CurrentPage, + Week=Week, + TmdbId=TmdbId, + PersonId=PersonId, + SubTitle=SubTitle, + Keyword=Keyword, + Source=Source, + Filter=FilterKey, + FilterConf=ModuleConf.DISCOVER_FILTER_CONF.get(FilterKey) if FilterKey else {}, + Params=Params) + + +# 推荐页面 +@App.route('/ranking', methods=['POST', 'GET']) +@login_required +def ranking(): + return render_template("discovery/ranking.html", + DiscoveryType="RANKING") + + +# 豆瓣电影 +@App.route('/douban_movie', methods=['POST', 'GET']) +@login_required +def douban_movie(): + return render_template("discovery/recommend.html", + Type="DOUBANTAG", + SubType="MOV", + Title="豆瓣电影", + Filter="douban_movie", + FilterConf=ModuleConf.DISCOVER_FILTER_CONF.get('douban_movie')) + + +# 豆瓣电视剧 +@App.route('/douban_tv', methods=['POST', 'GET']) +@login_required +def douban_tv(): + return render_template("discovery/recommend.html", + Type="DOUBANTAG", + SubType="TV", + Title="豆瓣电视剧", + Filter="douban_tv", + FilterConf=ModuleConf.DISCOVER_FILTER_CONF.get('douban_tv')) + + +@App.route('/tmdb_movie', methods=['POST', 'GET']) +@login_required +def tmdb_movie(): + return render_template("discovery/recommend.html", + Type="DISCOVER", + SubType="MOV", + Title="TMDB电影", + Filter="tmdb_movie", + FilterConf=ModuleConf.DISCOVER_FILTER_CONF.get('tmdb_movie')) + + +@App.route('/tmdb_tv', methods=['POST', 'GET']) +@login_required +def tmdb_tv(): + return render_template("discovery/recommend.html", + Type="DISCOVER", + SubType="TV", + Title="TMDB电视剧", + Filter="tmdb_tv", + FilterConf=ModuleConf.DISCOVER_FILTER_CONF.get('tmdb_tv')) + + +# Bangumi每日放送 +@App.route('/bangumi', methods=['POST', 'GET']) +@login_required +def discovery_bangumi(): + return render_template("discovery/ranking.html", + DiscoveryType="BANGUMI") + + +# 媒体详情页面 +@App.route('/media_detail', methods=['POST', 'GET']) +@login_required +def media_detail(): + TmdbId = request.args.get("id") + Type = request.args.get("type") + return render_template("discovery/mediainfo.html", + TmdbId=TmdbId, + Type=Type) + + +# 演职人员页面 +@App.route('/discovery_person', methods=['POST', 'GET']) +@login_required +def discovery_person(): + TmdbId = request.args.get("tmdbid") + Title = request.args.get("title") + SubTitle = request.args.get("subtitle") + Type = request.args.get("type") + return render_template("discovery/person.html", + TmdbId=TmdbId, + Title=Title, + SubTitle=SubTitle, + Type=Type) + + +# 正在下载页面 +@App.route('/downloading', methods=['POST', 'GET']) +@login_required +def downloading(): + DispTorrents = WebAction().get_downloading().get("result") + return render_template("download/downloading.html", + DownloadCount=len(DispTorrents), + Torrents=DispTorrents, + Client=Config().get_config("pt").get("pt_client")) + + +# 近期下载页面 +@App.route('/downloaded', methods=['POST', 'GET']) +@login_required +def downloaded(): + CurrentPage = request.args.get("page") or 1 + return render_template("discovery/recommend.html", + Type='DOWNLOADED', + Title='近期下载', + CurrentPage=CurrentPage) + + +@App.route('/torrent_remove', methods=['POST', 'GET']) +@login_required +def torrent_remove(): + TorrentRemoveTasks = TorrentRemover().get_torrent_remove_tasks() + return render_template("download/torrent_remove.html", + DownloaderConfig=ModuleConf.TORRENTREMOVER_DICT, + Count=len(TorrentRemoveTasks), + TorrentRemoveTasks=TorrentRemoveTasks) + + +# 数据统计页面 +@App.route('/statistics', methods=['POST', 'GET']) +@login_required +def statistics(): + # 刷新单个site + refresh_site = request.args.getlist("refresh_site") + # 强制刷新所有 + refresh_force = True if request.args.get("refresh_force") else False + # 总上传下载 + TotalUpload = 0 + TotalDownload = 0 + TotalSeedingSize = 0 + TotalSeeding = 0 + # 站点标签及上传下载 + SiteNames = [] + SiteUploads = [] + SiteDownloads = [] + SiteRatios = [] + SiteErrs = {} + # 站点上传下载 + SiteData = SiteUserInfo().get_pt_date(specify_sites=refresh_site, force=refresh_force) + if isinstance(SiteData, dict): + for name, data in SiteData.items(): + if not data: + continue + up = data.get("upload", 0) + dl = data.get("download", 0) + ratio = data.get("ratio", 0) + seeding = data.get("seeding", 0) + seeding_size = data.get("seeding_size", 0) + err_msg = data.get("err_msg", "") + + SiteErrs.update({name: err_msg}) + + if not up and not dl and not ratio: + continue + if not str(up).isdigit() or not str(dl).isdigit(): + continue + if name not in SiteNames: + SiteNames.append(name) + TotalUpload += int(up) + TotalDownload += int(dl) + TotalSeeding += int(seeding) + TotalSeedingSize += int(seeding_size) + SiteUploads.append(int(up)) + SiteDownloads.append(int(dl)) + SiteRatios.append(round(float(ratio), 1)) + + # 近期上传下载各站点汇总 + CurrentUpload, CurrentDownload, _, _, _ = SiteUserInfo().get_pt_site_statistics_history( + days=2) + + # 站点用户数据 + SiteUserStatistics = WebAction().get_site_user_statistics({"encoding": "DICT"}).get("data") + + return render_template("site/statistics.html", + CurrentDownload=CurrentDownload, + CurrentUpload=CurrentUpload, + TotalDownload=TotalDownload, + TotalUpload=TotalUpload, + TotalSeedingSize=TotalSeedingSize, + TotalSeeding=TotalSeeding, + SiteDownloads=SiteDownloads, + SiteUploads=SiteUploads, + SiteRatios=SiteRatios, + SiteNames=SiteNames, + SiteErr=SiteErrs, + SiteUserStatistics=SiteUserStatistics) + + +# 刷流任务页面 +@App.route('/brushtask', methods=['POST', 'GET']) +@login_required +def brushtask(): + # 站点列表 + CfgSites = Sites().get_sites(brush=True) + # 下载器列表 + Downloaders = BrushTask().get_downloader_info() + # 任务列表 + Tasks = BrushTask().get_brushtask_info() + return render_template("site/brushtask.html", + Count=len(Tasks), + Sites=CfgSites, + Tasks=Tasks, + Downloaders=Downloaders) + + +# 自定义下载器页面 +@App.route('/userdownloader', methods=['POST', 'GET']) +@login_required +def userdownloader(): + downloaders = BrushTask().get_downloader_info() + return render_template("download/userdownloader.html", + Count=len(downloaders), + Downloaders=downloaders) + + +# 服务页面 +@App.route('/service', methods=['POST', 'GET']) +@login_required +def service(): + scheduler_cfg_list = [] + RuleGroups = Filter().get_rule_groups() + pt = Config().get_config('pt') + if pt: + # RSS订阅 + pt_check_interval = pt.get('pt_check_interval') + if str(pt_check_interval).isdigit(): + tim_rssdownload = str(round(int(pt_check_interval) / 60)) + " 分钟" + rss_state = 'ON' + else: + tim_rssdownload = "" + rss_state = 'OFF' + svg = ''' + + + + + + + ''' + + scheduler_cfg_list.append( + {'name': 'RSS订阅', 'time': tim_rssdownload, 'state': rss_state, 'id': 'rssdownload', 'svg': svg, + 'color': "blue"}) + + search_rss_interval = pt.get('search_rss_interval') + if str(search_rss_interval).isdigit(): + if int(search_rss_interval) < 6: + search_rss_interval = 6 + tim_rsssearch = str(int(search_rss_interval)) + " 小时" + rss_search_state = 'ON' + else: + tim_rsssearch = "" + rss_search_state = 'OFF' + + svg = ''' + + + + + + ''' + + scheduler_cfg_list.append( + {'name': '订阅搜索', 'time': tim_rsssearch, 'state': rss_search_state, 'id': 'subscribe_search_all', + 'svg': svg, + 'color': "blue"}) + + # 下载文件转移 + pt_monitor = pt.get('pt_monitor') + if pt_monitor: + tim_pttransfer = str(round(PT_TRANSFER_INTERVAL / 60)) + " 分钟" + sta_pttransfer = 'ON' + else: + tim_pttransfer = "" + sta_pttransfer = 'OFF' + svg = ''' + + + + + + + + ''' + scheduler_cfg_list.append( + {'name': '下载文件转移', 'time': tim_pttransfer, 'state': sta_pttransfer, 'id': 'pttransfer', 'svg': svg, + 'color': "green"}) + + # 删种 + torrent_remove_tasks = TorrentRemover().get_torrent_remove_tasks() + if torrent_remove_tasks: + sta_autoremovetorrents = 'ON' + svg = ''' + + + + + + + + + ''' + scheduler_cfg_list.append( + {'name': '自动删种', 'state': sta_autoremovetorrents, + 'id': 'autoremovetorrents', 'svg': svg, 'color': "twitter"}) + + # 自动签到 + tim_ptsignin = pt.get('ptsignin_cron') + if tim_ptsignin: + if str(tim_ptsignin).find(':') == -1: + tim_ptsignin = "%s 小时" % tim_ptsignin + sta_ptsignin = 'ON' + svg = ''' + + + + + + + ''' + scheduler_cfg_list.append( + {'name': '站点签到', 'time': tim_ptsignin, 'state': sta_ptsignin, 'id': 'ptsignin', 'svg': svg, + 'color': "facebook"}) + + # 目录同步 + sync_paths = Sync().get_sync_dirs() + if sync_paths: + sta_sync = 'ON' + svg = ''' + + + + + + ''' + scheduler_cfg_list.append( + {'name': '目录同步', 'time': '实时监控', 'state': sta_sync, 'id': 'sync', 'svg': svg, + 'color': "orange"}) + # 豆瓣同步 + douban_cfg = Config().get_config('douban') + if douban_cfg: + interval = douban_cfg.get('interval') + if interval: + interval = "%s 小时" % interval + sta_douban = "ON" + svg = ''' + + + + + + ''' + scheduler_cfg_list.append( + {'name': '豆瓣想看', 'time': interval, 'state': sta_douban, 'id': 'douban', 'svg': svg, + 'color': "pink"}) + + # 清理文件整理缓存 + svg = ''' + + + + + + ''' + scheduler_cfg_list.append( + {'name': '清理转移缓存', 'time': '手动', 'state': 'OFF', 'id': 'blacklist', 'svg': svg, 'color': 'red'}) + + # 清理RSS缓存 + svg = ''' + + + + + + ''' + scheduler_cfg_list.append( + {'name': '清理RSS缓存', 'time': '手动', 'state': 'OFF', 'id': 'rsshistory', 'svg': svg, 'color': 'purple'}) + + # 名称识别测试 + svg = ''' + + + + + + + ''' + scheduler_cfg_list.append( + {'name': '名称识别测试', 'time': '', 'state': 'OFF', 'id': 'nametest', 'svg': svg, 'color': 'lime'}) + + # 过滤规则测试 + svg = ''' + + + + + + + + + + + + + ''' + scheduler_cfg_list.append( + {'name': '过滤规则测试', 'time': '', 'state': 'OFF', 'id': 'ruletest', 'svg': svg, 'color': 'yellow'}) + + # 网络连通性测试 + svg = ''' + + + + + + + + + + + + ''' + targets = ModuleConf.NETTEST_TARGETS + scheduler_cfg_list.append( + {'name': '网络连通性测试', 'time': '', 'state': 'OFF', 'id': 'nettest', 'svg': svg, 'color': 'cyan', + "targets": targets}) + + # 备份 + svg = ''' + + + + + + ''' + scheduler_cfg_list.append( + {'name': '备份&恢复', 'time': '', 'state': 'OFF', 'id': 'backup', 'svg': svg, 'color': 'green'}) + return render_template("service.html", + Count=len(scheduler_cfg_list), + RuleGroups=RuleGroups, + SchedulerTasks=scheduler_cfg_list) + + +# 历史记录页面 +@App.route('/history', methods=['POST', 'GET']) +@login_required +def history(): + pagenum = request.args.get("pagenum") + keyword = request.args.get("s") or "" + current_page = request.args.get("page") + Result = WebAction().get_transfer_history({"keyword": keyword, "page": current_page, "pagenum": pagenum}) + if Result.get("totalPage") <= 5: + StartPage = 1 + EndPage = Result.get("totalPage") + else: + if Result.get("currentPage") <= 3: + StartPage = 1 + EndPage = 5 + elif Result.get("currentPage") >= Result.get("totalPage") - 2: + StartPage = Result.get("totalPage") - 4 + EndPage = Result.get("totalPage") + else: + StartPage = Result.get("currentPage") - 2 + if Result.get("totalPage") > Result.get("currentPage") + 2: + EndPage = Result.get("currentPage") + 2 + else: + EndPage = Result.get("totalPage") + PageRange = range(StartPage, EndPage + 1) + + return render_template("rename/history.html", + TotalCount=Result.get("total"), + Count=len(Result.get("result")), + Historys=Result.get("result"), + Search=keyword, + CurrentPage=Result.get("currentPage"), + TotalPage=Result.get("totalPage"), + PageRange=PageRange, + PageNum=Result.get("currentPage")) + + +# TMDB缓存页面 +@App.route('/tmdbcache', methods=['POST', 'GET']) +@login_required +def tmdbcache(): + page_num = request.args.get("pagenum") + if not page_num: + page_num = 30 + search_str = request.args.get("s") + if not search_str: + search_str = "" + current_page = request.args.get("page") + if not current_page: + current_page = 1 + else: + current_page = int(current_page) + total_count, tmdb_caches = MetaHelper().dump_meta_data(search_str, current_page, page_num) + + total_page = floor(total_count / page_num) + 1 + + if total_page <= 5: + start_page = 1 + end_page = total_page + else: + if current_page <= 3: + start_page = 1 + end_page = 5 + else: + start_page = current_page - 3 + if total_page > current_page + 3: + end_page = current_page + 3 + else: + end_page = total_page + + page_range = range(start_page, end_page + 1) + + return render_template("rename/tmdbcache.html", + TotalCount=total_count, + Count=len(tmdb_caches), + TmdbCaches=tmdb_caches, + Search=search_str, + CurrentPage=current_page, + TotalPage=total_page, + PageRange=page_range, + PageNum=page_num) + + +# 手工识别页面 +@App.route('/unidentification', methods=['POST', 'GET']) +@login_required +def unidentification(): + Items = WebAction().get_unknown_list().get("items") + return render_template("rename/unidentification.html", + TotalCount=len(Items), + Items=Items) + + +# 文件管理页面 +@App.route('/mediafile', methods=['POST', 'GET']) +@login_required +def mediafile(): + download_dirs = Downloader().get_download_visit_dirs() + if download_dirs: + try: + DirD = os.path.commonpath(download_dirs).replace("\\", "/") + except Exception as err: + print(str(err)) + DirD = "/" + else: + DirD = "/" + DirR = request.args.get("dir") + return render_template("rename/mediafile.html", + Dir=DirR or DirD) + + +# 基础设置页面 +@App.route('/basic', methods=['POST', 'GET']) +@login_required +def basic(): + proxy = Config().get_config('app').get("proxies", {}).get("http") + if proxy: + proxy = proxy.replace("http://", "") + RmtModeDict = WebAction().get_rmt_modes() + CustomScriptCfg = SystemConfig().get_system_config("CustomScript") + return render_template("setting/basic.html", + Config=Config().get_config(), + Proxy=proxy, + RmtModeDict=RmtModeDict, + CustomScriptCfg=CustomScriptCfg) + + +# 自定义识别词设置页面 +@App.route('/customwords', methods=['POST', 'GET']) +@login_required +def customwords(): + groups = WebAction().get_customwords().get("result") + return render_template("setting/customwords.html", + Groups=groups, + GroupsCount=len(groups)) + + +# 目录同步页面 +@App.route('/directorysync', methods=['POST', 'GET']) +@login_required +def directorysync(): + RmtModeDict = WebAction().get_rmt_modes() + SyncPaths = WebAction().get_directorysync().get("result") + return render_template("setting/directorysync.html", + SyncPaths=SyncPaths, + SyncCount=len(SyncPaths), + RmtModeDict=RmtModeDict) + + +# 豆瓣页面 +@App.route('/douban', methods=['POST', 'GET']) +@login_required +def douban(): + DoubanHistory = WebAction().get_douban_history().get("result") + return render_template("setting/douban.html", + Config=Config().get_config(), + HistoryCount=len(DoubanHistory), + DoubanHistory=DoubanHistory) + + +# 下载器页面 +@App.route('/downloader', methods=['POST', 'GET']) +@login_required +def downloader(): + return render_template("setting/downloader.html", + Config=Config().get_config(), + SpeedLimitConf=SystemConfig().get_system_config("SpeedLimit") or {}, + DownloaderConf=ModuleConf.DOWNLOADER_CONF) + + +# 下载设置页面 +@App.route('/download_setting', methods=['POST', 'GET']) +@login_required +def download_setting(): + DownloadSetting = Downloader().get_download_setting() + DefaultDownloadSetting = Downloader().get_default_download_setting() + Count = len(DownloadSetting) + return render_template("setting/download_setting.html", + DownloadSetting=DownloadSetting, + DefaultDownloadSetting=DefaultDownloadSetting, + DownloaderTypes=DownloaderType, + Count=Count) + + +# 索引器页面 +@App.route('/indexer', methods=['POST', 'GET']) +@login_required +def indexer(): + indexers = Indexer().get_builtin_indexers(check=False) + private_count = len([item.id for item in indexers if not item.public]) + public_count = len([item.id for item in indexers if item.public]) + return render_template("setting/indexer.html", + Config=Config().get_config(), + PrivateCount=private_count, + PublicCount=public_count, + Indexers=indexers, + IndexerConf=ModuleConf.INDEXER_CONF) + + +# 媒体库页面 +@App.route('/library', methods=['POST', 'GET']) +@login_required +def library(): + return render_template("setting/library.html", Config=Config().get_config()) + + +# 媒体服务器页面 +@App.route('/mediaserver', methods=['POST', 'GET']) +@login_required +def mediaserver(): + return render_template("setting/mediaserver.html", + Config=Config().get_config(), + MediaServerConf=ModuleConf.MEDIASERVER_CONF) + + +# 通知消息页面 +@App.route('/notification', methods=['POST', 'GET']) +@login_required +def notification(): + MessageClients = Message().get_message_client_info() + Channels = ModuleConf.MESSAGE_CONF.get("client") + Switchs = ModuleConf.MESSAGE_CONF.get("switch") + return render_template("setting/notification.html", + Channels=Channels, + Switchs=Switchs, + ClientCount=len(MessageClients), + MessageClients=MessageClients) + + +# 字幕设置页面 +@App.route('/subtitle', methods=['POST', 'GET']) +@login_required +def subtitle(): + ChromeOk = ChromeHelper().get_status() + return render_template("setting/subtitle.html", + Config=Config().get_config(), + ChromeOk=ChromeOk) + + +# 用户管理页面 +@App.route('/users', methods=['POST', 'GET']) +@login_required +def users(): + Users = WebAction().get_users().get("result") + return render_template("setting/users.html", Users=Users, UserCount=len(Users)) + + +# 过滤规则设置页面 +@App.route('/filterrule', methods=['POST', 'GET']) +@login_required +def filterrule(): + result = WebAction().get_filterrules() + return render_template("setting/filterrule.html", + Count=len(result.get("ruleGroups")), + RuleGroups=result.get("ruleGroups"), + Init_RuleGroups=result.get("initRules")) + + +# 自定义订阅页面 +@App.route('/user_rss', methods=['POST', 'GET']) +@login_required +def user_rss(): + Tasks = RssChecker().get_rsstask_info() + RssParsers = RssChecker().get_userrss_parser() + RuleGroups = {str(group["id"]): group["name"] for group in Filter().get_rule_groups()} + DownloadSettings = {did: attr["name"] for did, attr in Downloader().get_download_setting().items()} + RestypeDict = ModuleConf.TORRENT_SEARCH_PARAMS.get("restype") + PixDict = ModuleConf.TORRENT_SEARCH_PARAMS.get("pix") + return render_template("rss/user_rss.html", + Tasks=Tasks, + Count=len(Tasks), + RssParsers=RssParsers, + RuleGroups=RuleGroups, + RestypeDict=RestypeDict, + PixDict=PixDict, + DownloadSettings=DownloadSettings) + + +# RSS解析器页面 +@App.route('/rss_parser', methods=['POST', 'GET']) +@login_required +def rss_parser(): + RssParsers = RssChecker().get_userrss_parser() + return render_template("rss/rss_parser.html", + RssParsers=RssParsers, + Count=len(RssParsers)) + + +# 事件响应 +@App.route('/do', methods=['POST']) +@action_login_check +def do(): + try: + cmd = request.form.get("cmd") + data = request.form.get("data") + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": -1, "msg": str(e)} + if data: + data = json.loads(data) + return WebAction().action(cmd, data) + + +# 目录事件响应 +@App.route('/dirlist', methods=['POST']) +@login_required +def dirlist(): + r = ['') + return make_response(''.join(r), 200) + + +# 禁止搜索引擎 +@App.route('/robots.txt', methods=['GET', 'POST']) +def robots(): + return send_from_directory("", "robots.txt") + + +# 响应企业微信消息 +@App.route('/wechat', methods=['GET', 'POST']) +def wechat(): + # 当前在用的交互渠道 + interactive_client = Message().get_interactive_client(SearchType.WX) + if not interactive_client: + return make_response("NAStool没有启用微信交互", 200) + conf = interactive_client.get("config") + sToken = conf.get('token') + sEncodingAESKey = conf.get('encodingAESKey') + sCorpID = conf.get('corpid') + if not sToken or not sEncodingAESKey or not sCorpID: + return + wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID) + sVerifyMsgSig = request.args.get("msg_signature") + sVerifyTimeStamp = request.args.get("timestamp") + sVerifyNonce = request.args.get("nonce") + + if request.method == 'GET': + if not sVerifyMsgSig and not sVerifyTimeStamp and not sVerifyNonce: + return "NAStool微信交互服务正常!
微信回调配置步聚:
1、在微信企业应用接收消息设置页面生成Token和EncodingAESKey并填入设置->消息通知->微信对应项,打开微信交互开关。
2、保存并重启本工具,保存并重启本工具,保存并重启本工具。
3、在微信企业应用接收消息设置页面输入此地址:http(s)://IP:PORT/wechat(IP、PORT替换为本工具的外网访问地址及端口,需要有公网IP并做好端口转发,最好有域名)。" + sVerifyEchoStr = request.args.get("echostr") + log.debug("收到微信验证请求: echostr= %s" % sVerifyEchoStr) + ret, sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr) + if ret != 0: + log.error("微信请求验证失败 VerifyURL ret: %s" % str(ret)) + # 验证URL成功,将sEchoStr返回给企业号 + return sEchoStr + else: + try: + sReqData = request.data + log.debug("收到微信消息:%s" % str(sReqData)) + ret, sMsg = wxcpt.DecryptMsg(sReqData, sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce) + if ret != 0: + log.error("解密微信消息失败 DecryptMsg ret = %s" % str(ret)) + return make_response("ok", 200) + # 解析XML报文 + """ + 1、消息格式: + + + + 1348831860 + + + 1234567890123456 + 1 + + 2、事件格式: + + + + 1348831860 + + + 1 + + """ + dom_tree = xml.dom.minidom.parseString(sMsg.decode('UTF-8')) + root_node = dom_tree.documentElement + # 消息类型 + msg_type = DomUtils.tag_value(root_node, "MsgType") + # 用户ID + user_id = DomUtils.tag_value(root_node, "FromUserName") + # 没的消息类型和用户ID的消息不要 + if not msg_type or not user_id: + log.info("收到微信心跳报文...") + return make_response("ok", 200) + # 解析消息内容 + content = "" + if msg_type == "event": + # 事件消息 + event_key = DomUtils.tag_value(root_node, "EventKey") + if event_key: + log.info("点击菜单:%s" % event_key) + keys = event_key.split('#') + if len(keys) > 2: + content = ModuleConf.WECHAT_MENU.get(keys[2]) + elif msg_type == "text": + # 文本消息 + content = DomUtils.tag_value(root_node, "Content", default="") + if content: + # 处理消息内容 + WebAction().handle_message_job(msg=content, + in_from=SearchType.WX, + user_id=user_id, + user_name=user_id) + return make_response(content, 200) + except Exception as err: + ExceptionUtils.exception_traceback(err) + log.error("微信消息处理发生错误:%s - %s" % (str(err), traceback.format_exc())) + return make_response("ok", 200) + + +# Plex Webhook +@App.route('/plex', methods=['POST']) +def plex_webhook(): + if not SecurityHelper().check_mediaserver_ip(request.remote_addr): + log.warn(f"非法IP地址的媒体服务器消息通知:{request.remote_addr}") + return '不允许的IP地址请求' + request_json = json.loads(request.form.get('payload', {})) + log.debug("收到Plex Webhook报文:%s" % str(request_json)) + ThreadHelper().start_thread(WebhookEvent().plex_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().plex_action, (request_json,)) + return 'Ok' + + +# Jellyfin Webhook +@App.route('/jellyfin', methods=['POST']) +def jellyfin_webhook(): + if not SecurityHelper().check_mediaserver_ip(request.remote_addr): + log.warn(f"非法IP地址的媒体服务器消息通知:{request.remote_addr}") + return '不允许的IP地址请求' + request_json = request.get_json() + log.debug("收到Jellyfin Webhook报文:%s" % str(request_json)) + ThreadHelper().start_thread(WebhookEvent().jellyfin_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().jellyfin_action, (request_json,)) + return 'Ok' + + +@App.route('/emby', methods=['POST']) +# Emby Webhook +def emby_webhook(): + if not SecurityHelper().check_mediaserver_ip(request.remote_addr): + log.warn(f"非法IP地址的媒体服务器消息通知:{request.remote_addr}") + return '不允许的IP地址请求' + request_json = json.loads(request.form.get('data', {})) + log.debug("收到Emby Webhook报文:%s" % str(request_json)) + ThreadHelper().start_thread(WebhookEvent().emby_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().emby_action, (request_json,)) + return 'Ok' + + +# Telegram消息响应 +@App.route('/telegram', methods=['POST', 'GET']) +def telegram(): + """ + { + 'update_id': , + 'message': { + 'message_id': , + 'from': { + 'id': , + 'is_bot': False, + 'first_name': '', + 'username': '', + 'language_code': 'zh-hans' + }, + 'chat': { + 'id': , + 'first_name': '', + 'username': '', + 'type': 'private' + }, + 'date': , + 'text': '' + } + } + """ + # 当前在用的交互渠道 + interactive_client = Message().get_interactive_client(SearchType.TG) + if not interactive_client: + return 'NAStool未启用Telegram交互' + msg_json = request.get_json() + if not SecurityHelper().check_telegram_ip(request.remote_addr): + log.error("收到来自 %s 的非法Telegram消息:%s" % (request.remote_addr, msg_json)) + return '不允许的IP地址请求' + if msg_json: + message = msg_json.get("message", {}) + text = message.get("text") + user_id = message.get("from", {}).get("id") + log.info("收到Telegram消息:from=%s, text=%s" % (user_id, text)) + # 获取用户名 + user_name = message.get("from", {}).get("username") + if text: + # 检查权限 + if text.startswith("/"): + if str(user_id) not in interactive_client.get("client").get_admin(): + Message().send_channel_msg(channel=SearchType.TG, + title="只有管理员才有权限执行此命令", + user_id=user_id) + return '只有管理员才有权限执行此命令' + else: + if not str(user_id) in interactive_client.get("client").get_users(): + message.send_channel_msg(channel=SearchType.TG, + title="你不在用户白名单中,无法使用此机器人", + user_id=user_id) + return '你不在用户白名单中,无法使用此机器人' + WebAction().handle_message_job(msg=text, + in_from=SearchType.TG, + user_id=user_id, + user_name=user_name) + return 'Ok' + + +# Synology Chat消息响应 +@App.route('/synology', methods=['POST', 'GET']) +def synology(): + """ + token: bot token + user_id + username + post_id + timestamp + text + """ + # 当前在用的交互渠道 + interactive_client = Message().get_interactive_client(SearchType.SYNOLOGY) + if not interactive_client: + return 'NAStool未启用Synology Chat交互' + msg_data = request.form + if not SecurityHelper().check_synology_ip(request.remote_addr): + log.error("收到来自 %s 的非法Synology Chat消息:%s" % (request.remote_addr, msg_data)) + return '不允许的IP地址请求' + if msg_data: + token = msg_data.get("token") + if not interactive_client.get("client").check_token(token): + log.error("收到来自 %s 的非法Synology Chat消息:token校验不通过!" % request.remote_addr) + return 'token校验不通过' + text = msg_data.get("text") + user_id = int(msg_data.get("user_id")) + log.info("收到Synology Chat消息:from=%s, text=%s" % (user_id, text)) + # 获取用户名 + user_name = msg_data.get("username") + if text: + WebAction().handle_message_job(msg=text, + in_from=SearchType.SYNOLOGY, + user_id=user_id, + user_name=user_name) + return 'Ok' + + +# Slack消息响应 +@App.route('/slack', methods=['POST']) +def slack(): + """ + # 消息 + { + 'client_msg_id': '', + 'type': 'message', + 'text': 'hello', + 'user': '', + 'ts': '1670143568.444289', + 'blocks': [{ + 'type': 'rich_text', + 'block_id': 'i2j+', + 'elements': [{ + 'type': 'rich_text_section', + 'elements': [{ + 'type': 'text', + 'text': 'hello' + }] + }] + }], + 'team': '', + 'client': '', + 'event_ts': '1670143568.444289', + 'channel_type': 'im' + } + # 快捷方式 + { + "type": "shortcut", + "token": "XXXXXXXXXXXXX", + "action_ts": "1581106241.371594", + "team": { + "id": "TXXXXXXXX", + "domain": "shortcuts-test" + }, + "user": { + "id": "UXXXXXXXXX", + "username": "aman", + "team_id": "TXXXXXXXX" + }, + "callback_id": "shortcut_create_task", + "trigger_id": "944799105734.773906753841.38b5894552bdd4a780554ee59d1f3638" + } + # 按钮点击 + { + "type": "block_actions", + "team": { + "id": "T9TK3CUKW", + "domain": "example" + }, + "user": { + "id": "UA8RXUSPL", + "username": "jtorrance", + "team_id": "T9TK3CUKW" + }, + "api_app_id": "AABA1ABCD", + "token": "9s8d9as89d8as9d8as989", + "container": { + "type": "message_attachment", + "message_ts": "1548261231.000200", + "attachment_id": 1, + "channel_id": "CBR2V3XEX", + "is_ephemeral": false, + "is_app_unfurl": false + }, + "trigger_id": "12321423423.333649436676.d8c1bb837935619ccad0f624c448ffb3", + "client": { + "id": "CBR2V3XEX", + "name": "review-updates" + }, + "message": { + "bot_id": "BAH5CA16Z", + "type": "message", + "text": "This content can't be displayed.", + "user": "UAJ2RU415", + "ts": "1548261231.000200", + ... + }, + "response_url": "https://hooks.slack.com/actions/AABA1ABCD/1232321423432/D09sSasdasdAS9091209", + "actions": [ + { + "action_id": "WaXA", + "block_id": "=qXel", + "text": { + "type": "plain_text", + "text": "View", + "emoji": true + }, + "value": "click_me_123", + "type": "button", + "action_ts": "1548426417.840180" + } + ] + } + """ + # 只有本地转发请求能访问 + if not SecurityHelper().check_slack_ip(request.remote_addr): + log.warn(f"非法IP地址的Slack消息通知:{request.remote_addr}") + return '不允许的IP地址请求' + + # 当前在用的交互渠道 + interactive_client = Message().get_interactive_client(SearchType.SLACK) + if not interactive_client: + return 'NAStool未启用Slack交互' + msg_json = request.get_json() + if msg_json: + if msg_json.get("type") == "message": + channel = msg_json.get("client") + text = msg_json.get("text") + username = "" + elif msg_json.get("type") == "block_actions": + channel = msg_json.get("client", {}).get("id") + text = msg_json.get("actions")[0].get("value") + username = msg_json.get("user", {}).get("name") + elif msg_json.get("type") == "event_callback": + channel = msg_json.get("event", {}).get("client") + text = re.sub(r"<@[0-9A-Z]+>", "", msg_json.get("event", {}).get("text"), flags=re.IGNORECASE).strip() + username = "" + elif msg_json.get("type") == "shortcut": + channel = "" + text = msg_json.get("callback_id") + username = msg_json.get("user", {}).get("username") + else: + return "Error" + WebAction().handle_message_job(msg=text, + in_from=SearchType.SLACK, + user_id=channel, + user_name=username) + return "Ok" + + +# Jellyseerr Overseerr订阅接口 +@App.route('/subscribe', methods=['POST', 'GET']) +@require_auth +def subscribe(): + """ + { + "notification_type": "{{notification_type}}", + "event": "{{event}}", + "subject": "{{subject}}", + "message": "{{message}}", + "image": "{{image}}", + "{{media}}": { + "media_type": "{{media_type}}", + "tmdbId": "{{media_tmdbid}}", + "tvdbId": "{{media_tvdbid}}", + "status": "{{media_status}}", + "status4k": "{{media_status4k}}" + }, + "{{request}}": { + "request_id": "{{request_id}}", + "requestedBy_email": "{{requestedBy_email}}", + "requestedBy_username": "{{requestedBy_username}}", + "requestedBy_avatar": "{{requestedBy_avatar}}" + }, + "{{issue}}": { + "issue_id": "{{issue_id}}", + "issue_type": "{{issue_type}}", + "issue_status": "{{issue_status}}", + "reportedBy_email": "{{reportedBy_email}}", + "reportedBy_username": "{{reportedBy_username}}", + "reportedBy_avatar": "{{reportedBy_avatar}}" + }, + "{{comment}}": { + "comment_message": "{{comment_message}}", + "commentedBy_email": "{{commentedBy_email}}", + "commentedBy_username": "{{commentedBy_username}}", + "commentedBy_avatar": "{{commentedBy_avatar}}" + }, + "{{extra}}": [] + } + """ + req_json = request.get_json() + if not req_json: + return make_response("非法请求!", 400) + notification_type = req_json.get("notification_type") + if notification_type not in ["MEDIA_APPROVED", "MEDIA_AUTO_APPROVED"]: + return make_response("ok", 200) + subject = req_json.get("subject") + media_type = MediaType.MOVIE if req_json.get("media", {}).get("media_type") == "movie" else MediaType.TV + tmdbId = req_json.get("media", {}).get("tmdbId") + if not media_type or not tmdbId or not subject: + return make_response("请求参数不正确!", 500) + # 添加订阅 + code = 0 + msg = "ok" + meta_info = MetaInfo(title=subject, mtype=media_type) + if media_type == MediaType.MOVIE: + code, msg, meta_info = Subscribe().add_rss_subscribe(mtype=media_type, + name=meta_info.get_name(), + year=meta_info.year, + mediaid=tmdbId) + meta_info.user_name = req_json.get("request", {}).get("requestedBy_username") + Message().send_rss_success_message(in_from=SearchType.API, + media_info=meta_info) + else: + seasons = [] + for extra in req_json.get("extra", []): + if extra.get("name") == "Requested Seasons": + seasons = [int(str(sea).strip()) for sea in extra.get("value").split(", ") if str(sea).isdigit()] + break + for season in seasons: + code, msg, meta_info = Subscribe().add_rss_subscribe(mtype=media_type, + name=meta_info.get_name(), + year=meta_info.year, + mediaid=tmdbId, + season=season) + Message().send_rss_success_message(in_from=SearchType.API, + media_info=meta_info) + if code == 0: + return make_response("ok", 200) + else: + return make_response(msg, 500) + + +# 备份配置文件 +@App.route('/backup', methods=['POST']) +@login_required +def backup(): + """ + 备份用户设置文件 + :return: 备份文件.zip_file + """ + try: + # 创建备份文件夹 + config_path = Path(Config().get_config_path()) + backup_file = f"bk_{time.strftime('%Y%m%d%H%M%S')}" + backup_path = config_path / "backup_file" / backup_file + backup_path.mkdir(parents=True) + # 把现有的相关文件进行copy备份 + shutil.copy(f'{config_path}/config.yaml', backup_path) + shutil.copy(f'{config_path}/default-category.yaml', backup_path) + shutil.copy(f'{config_path}/user.db', backup_path) + conn = sqlite3.connect(f'{backup_path}/user.db') + cursor = conn.cursor() + # 执行操作删除不需要备份的表 + table_list = [ + 'SEARCH_RESULT_INFO', + 'RSS_TORRENTS', + 'DOUBAN_MEDIAS', + 'TRANSFER_HISTORY', + 'TRANSFER_UNKNOWN', + 'TRANSFER_BLACKLIST', + 'SYNC_HISTORY', + 'DOWNLOAD_HISTORY', + 'alembic_version' + ] + for table in table_list: + cursor.execute(f"""DROP TABLE IF EXISTS {table};""") + conn.commit() + cursor.close() + conn.close() + zip_file = str(backup_path) + '.zip' + if os.path.exists(zip_file): + zip_file = str(backup_path) + '.zip' + shutil.make_archive(str(backup_path), 'zip', str(backup_path)) + shutil.rmtree(str(backup_path)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + return make_response("创建备份失败", 400) + return send_file(zip_file) + + +# 上传文件到服务器 +@App.route('/upload', methods=['POST']) +@login_required +def upload(): + try: + files = request.files['file'] + temp_path = Config().get_temp_path() + if not os.path.exists(temp_path): + os.makedirs(temp_path) + file_path = Path(temp_path) / files.filename + files.save(str(file_path)) + return {"code": 0, "filepath": str(file_path)} + except Exception as e: + ExceptionUtils.exception_traceback(e) + return {"code": 1, "msg": str(e), "filepath": ""} + + +# base64模板过滤器 +@App.template_filter('b64encode') +def b64encode(s): + return base64.b64encode(s.encode()).decode() + + +# split模板过滤器 +@App.template_filter('split') +def split(string, char, pos): + return string.split(char)[pos] + + +# 刷流规则过滤器 +@App.template_filter('brush_rule_string') +def brush_rule_string(rules): + return WebAction.parse_brush_rule_string(rules) + + +# 大小格式化过滤器 +@App.template_filter('str_filesize') +def str_filesize(size): + return StringUtils.str_filesize(size, pre=1) + + +# MD5 HASH过滤器 +@App.template_filter('hash') +def md5_hash(text): + return StringUtils.md5_hash(text) diff --git a/web/robots.txt b/web/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/web/security.py b/web/security.py new file mode 100644 index 0000000..17d0818 --- /dev/null +++ b/web/security.py @@ -0,0 +1,118 @@ +import datetime +from functools import wraps + +import jwt +from flask import request + +from app.utils import TokenCache +from config import Config + + +def require_auth(func): + """ + API安全认证 + """ + + @wraps(func) + def wrapper(*args, **kwargs): + auth = request.headers.get("Authorization") + if auth: + auth = str(auth).split()[-1] + if auth == Config().get_config("security").get("api_key"): + return func(*args, **kwargs) + return { + "code": 401, + "success": False, + "message": "安全认证未通过,请检查ApiKey" + } + + return wrapper + + +def generate_access_token(username: str, algorithm: str = 'HS256', exp: float = 2): + """ + 生成access_token + :param username: 用户名(自定义部分) + :param algorithm: 加密算法 + :param exp: 过期时间,默认2小时 + :return:token + """ + + now = datetime.datetime.utcnow() + exp_datetime = now + datetime.timedelta(hours=exp) + access_payload = { + 'exp': exp_datetime, + 'iat': now, + 'username': username + } + access_token = jwt.encode(access_payload, + Config().get_config("security").get("api_key"), + algorithm=algorithm) + return access_token + + +def __decode_auth_token(token: str, algorithms='HS256'): + """ + 解密token + :param token:token字符串 + :return: 是否有效,playload + """ + key = Config().get_config("security").get("api_key") + try: + payload = jwt.decode(token, + key=key, + algorithms=algorithms) + except jwt.ExpiredSignatureError: + return False, jwt.decode(token, + key=key, + algorithms=algorithms, + options={'verify_exp': False}) + except (jwt.DecodeError, jwt.InvalidTokenError, jwt.ImmatureSignatureError): + return False, {} + else: + return True, payload + + +def identify(auth_header: str): + """ + 用户鉴权,返回是否有效、用户名 + """ + flag = False + if auth_header: + flag, payload = __decode_auth_token(auth_header) + if payload: + return flag, payload.get("username") or "" + return flag, "" + + +def login_required(func): + """ + 登录保护,验证用户是否登录 + :param func: + :return: + """ + + @wraps(func) + def wrapper(*args, **kwargs): + + def auth_failed(): + return { + "code": 403, + "success": False, + "message": "安全认证未通过,请检查Token" + } + + token = request.headers.get("Authorization", default=None) + if not token: + return auth_failed() + latest_token = TokenCache.get(token) + if not latest_token: + return auth_failed() + flag, username = identify(latest_token) + if not username: + return auth_failed() + if not flag and username: + TokenCache.set(token, generate_access_token(username)) + return func(*args, **kwargs) + + return wrapper diff --git a/web/static/.DS_Store b/web/static/.DS_Store new file mode 100644 index 0000000..940e942 Binary files /dev/null and b/web/static/.DS_Store differ diff --git a/web/static/components/card/index.js b/web/static/components/card/index.js new file mode 100644 index 0000000..f79bebb --- /dev/null +++ b/web/static/components/card/index.js @@ -0,0 +1,2 @@ +export * from "./normal/index.js"; +export * from "./person/index.js"; \ No newline at end of file diff --git a/web/static/components/card/normal/index.js b/web/static/components/card/normal/index.js new file mode 100644 index 0000000..17439c6 --- /dev/null +++ b/web/static/components/card/normal/index.js @@ -0,0 +1,189 @@ +import { NormalCardPlaceholder } from "./placeholder.js"; export { NormalCardPlaceholder }; + +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; +import { observeState } from "../../utility/lit-state.js"; +import { cardState } from "./state.js"; + +export class NormalCard extends observeState(CustomElement) { + + static properties = { + tmdb_id: { attribute: "card-tmdbid" }, + res_type: { attribute: "card-restype" }, + media_type: { attribute: "card-mediatype" }, + show_sub: { attribute: "card-showsub"}, + title: { attribute: "card-title" }, + fav: { attribute: "card-fav" , reflect: true}, + date: { attribute: "card-date" }, + vote: { attribute: "card-vote" }, + image: { attribute: "card-image" }, + overview: { attribute: "card-overview" }, + year: { attribute: "card-year" }, + site: { attribute: "card-site" }, + weekday: { attribute: "card-weekday" }, + lazy: {}, + _placeholder: { state: true }, + _card_id: { state: true }, + _card_image_error: { state: true }, + }; + + constructor() { + super(); + this.lazy = "0"; + this._placeholder = true; + this._card_image_error = false; + this._card_id = Symbol("normalCard_data_card_id"); + } + + _render_left_up() { + if (this.weekday || this.res_type) { + let color; + let text; + if (this.weekday) { + color = "bg-orange"; + text = this.weekday; + } else if (this.res_type) { + color = this.res_type === "电影" ? "bg-lime" : "bg-blue"; + text = this.res_type; + } + return html` + + ${text} + `; + } else { + return nothing; + } + } + + _render_right_up() { + if (this.fav == "2") { + return html` +
+ + + + +
`; + } else if (this.vote && this.vote != "0.0" && this.vote != "0") { + return html` +
+ ${this.vote} +
`; + } else { + return nothing; + } + } + + _render_bottom() { + if (this.show_sub == "1") { + return html` + `; + } else { + return nothing; + } + } + + render() { + return html` +
{ if (Golbal.is_touch_device()){ cardState.more_id = this._card_id } } } + @mouseenter=${() => { if (!Golbal.is_touch_device()){ cardState.more_id = this._card_id } } } + @mouseleave=${() => { if (!Golbal.is_touch_device()){ cardState.more_id = undefined } } }> + ${this._placeholder ? NormalCardPlaceholder.render_placeholder() : nothing} +
+ { if (this.lazy != "1") {this.image = Golbal.noImage; this._card_image_error = true} }} + @load=${() => { this._placeholder = false }}/> + ${this._render_left_up()} + ${this._render_right_up()} +
+
{ navmenu(`media_detail?type=${this.media_type}&id=${this.tmdb_id}`) }}> +
+ ${this.year ? html`
${this.site ? this.site : this.year}
` : nothing } + ${this.title + ? html` +

+ ${this.title} +

` + : nothing } + ${this.overview + ? html` +

+ ${this.overview} +

` + : nothing } + ${this.date + ? html` +

+ ${this.date} +

` + : nothing } +
+ ${this._render_bottom()} +
+
+ `; + } + + _fav_change() { + const options = { + detail: { + fav: this.fav + }, + bubbles: true, + composed: true, + }; + this.dispatchEvent(new CustomEvent("fav_change", options)); + } + + _loveClick(e) { + e.stopPropagation(); + Golbal.lit_love_click(this.title, this.year, this.media_type, this.tmdb_id, this.fav, + () => { + this.fav = "0"; + this._fav_change(); + }, + () => { + this.fav = "1"; + this._fav_change(); + }); + } + +} + +window.customElements.define("normal-card", NormalCard); \ No newline at end of file diff --git a/web/static/components/card/normal/placeholder.js b/web/static/components/card/normal/placeholder.js new file mode 100644 index 0000000..724ed30 --- /dev/null +++ b/web/static/components/card/normal/placeholder.js @@ -0,0 +1,26 @@ +import { html } from "../../utility/lit-core.min.js"; +import { CustomElement } from "../../utility/utility.js"; + +export class NormalCardPlaceholder extends CustomElement { + constructor() { + super(); + } + + static render_placeholder() { + return html` +
+
+
+ `; + } + + render() { + return html` +
+ ${NormalCardPlaceholder.render_placeholder()} +
+ `; + } +} + +window.customElements.define("normal-card-placeholder", NormalCardPlaceholder); \ No newline at end of file diff --git a/web/static/components/card/normal/state.js b/web/static/components/card/normal/state.js new file mode 100644 index 0000000..3ac5542 --- /dev/null +++ b/web/static/components/card/normal/state.js @@ -0,0 +1,11 @@ +import { LitState } from "../../utility/lit-state.js" + +class CardState extends LitState { + static get stateVars() { + return { + more_id: undefined + }; + } +} + +export const cardState = new CardState(); \ No newline at end of file diff --git a/web/static/components/card/person/index.js b/web/static/components/card/person/index.js new file mode 100644 index 0000000..7d3cde1 --- /dev/null +++ b/web/static/components/card/person/index.js @@ -0,0 +1,47 @@ +import { html } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; + +export class PersonCard extends CustomElement { + + static properties = { + person_id: { attribute: "person-id" }, + person_image: { attribute: "person-image" }, + person_name: { attribute: "person-name" }, + person_role: { attribute: "person-role" }, + lazy: {}, + }; + + constructor() { + super(); + this.lazy = "0"; + } + + render() { + return html` +
+
+
+ +
+

+ ${this.person_name} +

+
+ ${this.person_role} +
+
+
+ `; + } + +} + +window.customElements.define("person-card", PersonCard); \ No newline at end of file diff --git a/web/static/components/custom/chips/index.html b/web/static/components/custom/chips/index.html new file mode 100644 index 0000000..1d99287 --- /dev/null +++ b/web/static/components/custom/chips/index.html @@ -0,0 +1,147 @@ + \ No newline at end of file diff --git a/web/static/components/custom/img/index.js b/web/static/components/custom/img/index.js new file mode 100644 index 0000000..8f4a1ea --- /dev/null +++ b/web/static/components/custom/img/index.js @@ -0,0 +1,101 @@ +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; + +export class CustomImg extends CustomElement { + + static properties = { + img_src: { attribute: "img-src" }, + img_noimage: { attribute: "img-noimage" }, + img_class: { attribute: "img-class" }, + img_style: { attribute: "img-style" }, + img_ratio: { attribute: "img-ratio" }, + div_style: { attribute: "div-style" }, + img_placeholder: { attribute: "img-placeholder" }, + img_error: { attribute: "img-error" }, + img_src_list: { type: Array }, + lazy: {}, + _placeholder: { state: true }, + _timeout_update_img: { state: true }, + }; + + constructor() { + super(); + this.img_noimage = Golbal.noImage; + this.lazy = "0"; + this.img_placeholder = "1"; + this.img_error = "1"; + this.img_src_list = []; + this._timeout_update_img = 0; + this._placeholder = true; + } + + willUpdate(changedProperties) { + if (changedProperties.has("img_src")) { + this._placeholder = true; + } + if (changedProperties.has("img_src_list")) { + this._timeout_update_img = 0; + this._update_img(); + } + } + + firstUpdated() { + this._query_img = this.querySelector("img"); + } + + _update_img() { + if (this.img_src_list) { + if (this.img_src_list.length > 1) { + this._query_img.classList.remove("lit-custom-img-carousel-show"); + setTimeout(() => { + this.img_src = this.img_src_list[this._timeout_update_img]; + this._timeout_update_img ++; + if (this._timeout_update_img >= this.img_src_list.length) { + this._timeout_update_img = 0; + } + }, 1000); + } else if (this.img_src_list.length == 1) { + this.img_src = this.img_src_list[0]; + } + } + } + + render() { + return html` + +
+
+ { if (this.lazy != "1" && this.img_error == "1") { this.img_src = this.img_noimage } }} + @load=${() => { + this._placeholder = false; + // 图像渐入 + if (this.img_src_list.length > 0) { + this._query_img.classList.add("lit-custom-img-carousel"); + setTimeout(() => { + this._query_img.classList.add("lit-custom-img-carousel-show"); + setTimeout(() => { + this._update_img(); + }, 7000); + }, 100); + } + }}/> +
+ `; + } + +} + +window.customElements.define("custom-img", CustomImg); \ No newline at end of file diff --git a/web/static/components/custom/index.js b/web/static/components/custom/index.js new file mode 100644 index 0000000..788f5ef --- /dev/null +++ b/web/static/components/custom/index.js @@ -0,0 +1,2 @@ +export * from "./img/index.js"; +export * from "./slide/index.js"; \ No newline at end of file diff --git a/web/static/components/custom/slide/index.js b/web/static/components/custom/slide/index.js new file mode 100644 index 0000000..546a5ea --- /dev/null +++ b/web/static/components/custom/slide/index.js @@ -0,0 +1,178 @@ +import { html } from "../../utility/lit-core.min.js"; +import { CustomElement } from "../../utility/utility.js"; + +export class CustomSlide extends CustomElement { + + static properties = { + slide_title: { attribute: "slide-title" }, + slide_click: { attribute: "slide-click" }, + lazy: { attribute: "lazy" }, + //slide_scroll: { attribute: "slide-scroll" , reflect: true, type: Number }, + slide_card: { type: Array }, + _disabled: { state: true }, + }; + + constructor() { + super(); + this._disabled = 0; + this.slide_title = "加载中.."; + this.slide_click = "javascript:void(0)"; + this.slide_card = Array(20).fill(html``); + } + + render() { + return html` + + + `; + } + + updated(changedProperties) { + // slide数据刷新时触发界面状态改变 + if (changedProperties.has("slide_card")) { + this._countDisabled(); + } + } + + // 绑定事件 + firstUpdated() { + this._scrollbar = this.querySelector("div.media-slide-hide-scrollbar"); + this._card_number = this.querySelector("div.media-slide-card-number"); + // 初次获取元素参数 + this._countMaxNumber(); + // 窗口大小发生改变时 + this._countMaxNumber_resize = () => { this._countMaxNumber() }; // 防止无法卸载事件 + window.addEventListener("resize", this._countMaxNumber_resize); + } + + // 卸载事件 + disconnectedCallback() { + window.removeEventListener("resize", this._countMaxNumber_resize); + super.disconnectedCallback(); + } + + _countMaxNumber() { + this._card_width = this._card_number.getBoundingClientRect().width; + this._card_max = Math.trunc(this._scrollbar.clientWidth / this._card_width); + this._card_current_load_index = 0; + this._countDisabled(); + } + + _countDisabled() { + this._card_current = this._scrollbar.scrollLeft == 0 ? 0 : Math.trunc((this._scrollbar.scrollLeft + this._card_width / 2) / this._card_width) + if (this.slide_card.length * this._card_width <= this._scrollbar.clientWidth){ + this._disabled = 3; + } else if (this._scrollbar.scrollLeft == 0) { + this._disabled = 0; + } else if (this._scrollbar.scrollLeft >= this._scrollbar.scrollWidth - this._scrollbar.clientWidth - 2){ + this._disabled = 2; + } else { + this._disabled = 1; + } + // 懒加载 + if (this.lazy) { + if (this._card_current > this._card_current_load_index - this._card_max) { + const card_list = this._card_number.querySelectorAll(this.lazy); + if (card_list.length > 0) { + const show_max = this._card_current + this._card_max + 1; + for (let i = this._card_current; i < show_max; i++) { + if (i >= card_list.length) { + break; + } + card_list[i].removeAttribute("lazy"); + } + this._card_current_load_index = show_max; + } + } + } + } + + _slideNext(next) { + let run_to_left_px; + if (next) { + const card_index = this._card_current + this._card_max; + run_to_left_px = card_index * this._card_width; + if (run_to_left_px >= this._scrollbar.scrollWidth - this._scrollbar.clientWidth) { + run_to_left_px = this._scrollbar.scrollWidth - this._scrollbar.clientWidth; + } + } else { + const card_index = this._card_current - this._card_max; + run_to_left_px = card_index * this._card_width; + if (run_to_left_px <= 0) { + run_to_left_px = 0; + } + } + $(this._scrollbar).animate({ + scrollLeft: run_to_left_px + }, 350, () => { + this._scrollbar.scrollLeft = run_to_left_px; + }); + } + + +} + +window.customElements.define("custom-slide", CustomSlide); \ No newline at end of file diff --git a/web/static/components/index.js b/web/static/components/index.js new file mode 100644 index 0000000..2cd1cfb --- /dev/null +++ b/web/static/components/index.js @@ -0,0 +1,11 @@ +// 导入所有组件 +const body_div = document.createElement("div"); +[ +"custom/chips/index.html", +] +.forEach((name) => { + const my_wc = document.createElement("div"); + $(my_wc).load("../static/components/" + name); + body_div.appendChild(my_wc); +}) +document.body.appendChild(body_div); \ No newline at end of file diff --git a/web/static/components/layout/index.js b/web/static/components/layout/index.js new file mode 100644 index 0000000..342a219 --- /dev/null +++ b/web/static/components/layout/index.js @@ -0,0 +1,2 @@ +export * from "./navbar/index.js"; +export * from "./searchbar/index.js"; \ No newline at end of file diff --git a/web/static/components/layout/navbar/button.js b/web/static/components/layout/navbar/button.js new file mode 100644 index 0000000..691b095 --- /dev/null +++ b/web/static/components/layout/navbar/button.js @@ -0,0 +1,16 @@ +import { html } from "../../utility/lit-core.min.js"; +import { CustomElement } from "../../utility/utility.js"; + + +export class LayoutNavbarButton extends CustomElement { + render() { + return html` + + `; + } +} + + +window.customElements.define("layout-navbar-button", LayoutNavbarButton); \ No newline at end of file diff --git a/web/static/components/layout/navbar/index.js b/web/static/components/layout/navbar/index.js new file mode 100644 index 0000000..4dd1b80 --- /dev/null +++ b/web/static/components/layout/navbar/index.js @@ -0,0 +1,795 @@ +import { LayoutNavbarButton } from "./button.js"; export { LayoutNavbarButton }; +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement } from "../../utility/utility.js"; + +// name: 服务原名 +// page: 导航路径 +// icon: 项目图标 +// : 显示别名 (可选) +const navbar_list = [ + { + name: "我的媒体库", + page: "index", + icon: html` + + `, + }, + { + name: "探索", + list: [ + { + name: "榜单推荐", + page: "ranking", + icon: html` + + + + + + + + + `, + }, + { + name: "豆瓣电影", + page: "douban_movie", + icon: html` + + + + + + + + + + + + + `, + }, + { + name: "豆瓣电视剧", + page: "douban_tv", + icon: html` + + + + + + + `, + }, + { + name: "TMDB电影", + page: "tmdb_movie", + icon: html` + + + + + + + + + + + + + `, + }, + { + name: "TMDB电视剧", + page: "tmdb_tv", + icon: html` + + + + + + + `, + }, + { + name: "BANGUMI", + page: "bangumi", + icon: html` + + + + + + + + + + `, + }, + ], + }, + { + name: "资源搜索", + page: "search", + icon: html` + + `, + }, + { + name: "站点管理", + list: [ + { + name: "站点维护", + page: "site", + icon: html` + + `, + }, + { + name: "数据统计", + page: "statistics", + icon: html` + + + + + + + `, + }, + { + name: "刷流任务", + page: "brushtask", + icon: html` + + + + + + + + + `, + }, + { + name: "站点资源", + page: "sitelist", + icon: html` + + + + + + + + + `, + }, + ], + }, + { + name: "订阅管理", + list: [ + { + name: "电影订阅", + page: "movie_rss", + icon: html` + + + + + + + + + + + + + `, + }, + { + name: "电视剧订阅", + page: "tv_rss", + icon: html` + + + + + + + `, + }, + { + name: "自定义订阅", + page: "user_rss", + icon: html` + + + + + + + + + + `, + }, + { + name: "订阅日历", + page: "rss_calendar", + icon: html` + + + + + + + + + + + `, + }, + ], + }, + { + name: "下载管理", + list: [ + { + name: "正在下载", + page: "downloading", + icon: html` + + + + + + + + + + + + + `, + }, + { + name: "近期下载", + page: "downloaded", + icon: html` + + `, + }, + { + name: "自动删种", + page: "torrent_remove", + icon: html` + + + + + + + + + `, + }, + ], + }, + { + name: "媒体整理", + list: [ + { + name: "文件管理", + page: "mediafile", + icon: html` + + + + + + + + `, + }, + { + name: "手动识别", + page: "unidentification", + icon: html` + + + + + + + + `, + }, + { + name: "历史记录", + page: "history", + icon: html` + + + + + + + `, + }, + { + name: "TMDB缓存", + page: "tmdbcache", + icon: html` + + + + + + + `, + }, + ], + }, + { + name: "服务", + page: "service", + icon: html` + + `, + }, + { + name: "系统设置", + also: "设置", + list: [ + { + name: "基础设置", + page: "basic", + icon: html` + + `, + }, + { + name: "用户管理", + page: "users", + icon: html` + + + + + + + + + `, + }, + { + name: "媒体库", + page: "library", + icon: html` + + + + + + + + + + `, + }, + { + name: "目录同步", + page: "directorysync", + icon: html` + + + + + + + `, + }, + { + name: "消息通知", + page: "notification", + icon: html` + + + + + + + `, + }, + { + name: "过滤规则", + page: "filterrule", + icon: html` + + + + + + `, + }, + { + name: "自定义识别词", + page: "customwords", + icon: html` + + + + + + + + `, + }, + { + name: "索引器", + page: "indexer", + icon: html` + + + + + + + + + + `, + }, + { + name: "下载器", + page: "downloader", + icon: html` + + + + + + + + `, + }, + { + name: "媒体服务器", + page: "mediaserver", + icon: html` + + + + + + + + + + + + + + + + `, + }, + { + name: "字幕", + page: "subtitle", + icon: html` + + + + + + + + `, + }, + { + name: "豆瓣", + page: "douban", + icon: html` + + + + + + + + + + `, + }, + ], + }, +]; + +export class LayoutNavbar extends CustomElement { + static properties = { + layout_gopage: { attribute: "layout-gopage" }, + layout_appversion: { attribute: "layout-appversion"}, + layout_userpris: { attribute: "layout-userpris", type: Array }, + _active_name: { state: true}, + _update_appversion: { state: true }, + _update_url: { state: true }, + _is_update: { state: true }, + }; + + constructor() { + super(); + this.layout_gopage = ""; + this.layout_appversion = "v2.8.3 e950041"; + this.layout_userpris = navbar_list.map((item) => (item.name)); + this._active_name = ""; + this._update_appversion = ""; + this._update_url = "https://github.com/NAStool/nas-tools"; + this._is_update = false; + this.classList.add("navbar","navbar-vertical","navbar-expand-lg","lit-navbar-fixed","lit-navbar","lit-navbar-hide-scrollbar"); + } + + firstUpdated() { + // 加载页面 + if (this.layout_gopage) { + navmenu(this.layout_gopage); + } else if (window.history.state?.page) { + //console.log("刷新页面"); + window_history_refresh(); + } else { + // 打开第一个页面 + for (const item of navbar_list) { + if (item.name === this.layout_userpris[0]) { + navmenu(item.page ?? item.list[0].page); + break; + } + } + // 默认展开探索 + setTimeout(() => { this.show_collapse("ranking") }, 200); + } + // 删除logo动画 加点延迟切换体验好 + setTimeout(() => { + document.querySelector("#logo_animation").remove(); + this.removeAttribute("hidden"); + document.querySelector("#page_content").removeAttribute("hidden"); + document.querySelector("layout-searchbar").removeAttribute("hidden"); + }, 200); + // 检查更新 + if (this.layout_userpris.includes("系统设置")) { + this._check_new_version(); + } + } + + _check_new_version() { + ajax_post("version", {}, (ret) => { + if (ret.code === 0) { + let url = null; + switch (compareVersion(ret.version, this.layout_appversion)) { + case 1: + url = ret.url; + break; + case 2: + url = "https://github.com/NAStool/nas-tools/commits/master" + break; + } + if (url) { + this._update_url = url; + this._update_appversion = ret.version; + this._is_update = true; + } + } + }); + } + + update_active(page) { + this._active_name = page ?? window.history.state?.page; + this.show_collapse(this._active_name); + } + + show_collapse(page) { + for (const item of this.querySelectorAll("[id^='lit-navbar-collapse-']")) { + for (const a of item.querySelectorAll("a")) { + if (page === a.getAttribute("data-lit-page")) { + item.classList.add("show"); + this.querySelectorAll(`button[data-bs-target='#${item.id}']`)[0].classList.remove("collapsed"); + return; + } + } + } + } + + render() { + return html` + +
+
+
+
+

+ +

+
+ ${navbar_list.map((item, index) => ( html` + ${this.layout_userpris.includes(item.name) + ? html` + ${item.list?.length > 0 + ? html` + +
+ ${item.list.map((drop) => (this._render_page_item(drop, true)))} +
` + : this._render_page_item(item, false) + } ` + : nothing } + `))} +
+
+ + + + + + + + ${!this._is_update ? this.layout_appversion : html`${this.layout_appversion}`} + + + ${this._is_update + ? html` + { + e.preventDefault(); + e.stopPropagation(); + update(this._update_appversion); + return false; + }}> + + + + + ` + : nothing } + +
+
+
+
+
+ `; + } + + _render_page_item(item, child) { + return html` + { navmenu(item.page) }}> + + ${item.icon ?? nothing} + + + ${item.also ?? item.name} + + ` + } + +} + + +window.customElements.define("layout-navbar", LayoutNavbar); \ No newline at end of file diff --git a/web/static/components/layout/searchbar/index.js b/web/static/components/layout/searchbar/index.js new file mode 100644 index 0000000..b8ccb59 --- /dev/null +++ b/web/static/components/layout/searchbar/index.js @@ -0,0 +1,182 @@ +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement } from "../../utility/utility.js"; + +const search_source_icon = { + tmdb: html` + + + + + + + `, + douban: html` + + + + + + + ` +} + +export class LayoutSearchbar extends CustomElement { + static properties = { + layout_systemflag: { attribute: "layout-systemflag" }, + layout_username: { attribute: "layout-username" }, + layout_search_source: { attribute: "layout-search-source" }, + layout_userpris: { attribute: "layout-userpris", type: Array }, + _search_source: { state: true }, + }; + + constructor() { + super(); + this.layout_systemflag = "Docker"; + this.layout_username = "admin"; + this.layout_userpris = ["系统设置"]; + this.layout_search_source = "tmdb"; + this._search_source = "tmdb"; + this.classList.add("navbar", "fixed-top", "lit-searchbar"); + } + + firstUpdated() { + this._search_source = localStorage.getItem("SearchSource") ?? this.layout_search_source; + // 当前状态:是否模糊 + let blur = false; + window.addEventListener("scroll", () => { + const scroll_length = document.body.scrollTop || window.pageYOffset; + // 滚动发生时改变模糊状态 + if (!blur && scroll_length >= 5) { + // 模糊状态 + blur = true; + this.classList.add("lit-searchbar-blur"); + } else if (blur && scroll_length < 5) { + // 非模糊状态 + blur = false + this.classList.remove("lit-searchbar-blur"); + } + }); + + } + + // 卸载事件 + disconnectedCallback() { + super.disconnectedCallback(); + } + + get input() { + return this.querySelector(".home_search_bar") ?? null; + } + + render() { + return html` + + + `; + } + +} + + +window.customElements.define("layout-searchbar", LayoutSearchbar); \ No newline at end of file diff --git a/web/static/components/lit-index.js b/web/static/components/lit-index.js new file mode 100644 index 0000000..a349bbb --- /dev/null +++ b/web/static/components/lit-index.js @@ -0,0 +1,4 @@ +export * from "./custom/index.js"; +export * from "./card/index.js"; +export * from "./page/index.js"; +export * from "./layout/index.js"; \ No newline at end of file diff --git a/web/static/components/page/discovery/index.js b/web/static/components/page/discovery/index.js new file mode 100644 index 0000000..088c0bb --- /dev/null +++ b/web/static/components/page/discovery/index.js @@ -0,0 +1,161 @@ +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; + +export class PageDiscovery extends CustomElement { + static properties = { + discovery_type: { attribute: "discovery-type" }, + _slide_card_list: { state: true }, + _media_type_list: { state: true }, + }; + + constructor() { + super(); + this._slide_card_list = {}; + this._media_type_list = { + "RANKING": [ + { + type: "MOV", + title:"正在热映", + subtype :"dbom", + }, + { + type: "MOV", + title:"即将上映", + subtype :"dbnm", + }, + { + type: "TRENDING", + title:"TMDB流行趋势", + subtype :"tmdb", + }, + { + type: "MOV", + title:"豆瓣最新电影", + subtype :"dbnm", + }, + { + type: "MOV", + title:"豆瓣热门电影", + subtype :"dbhm", + }, + { + type: "MOV", + title:"豆瓣电影TOP250", + subtype :"dbtop", + }, + { + type: "TV", + title:"豆瓣热门电视剧", + subtype :"dbht", + }, + { + type: "TV", + title:"华语口碑剧集榜", + subtype :"dbct", + }, + { + type: "TV", + title:"全球口碑剧集榜", + subtype :"dbgt", + } + ], + "BANGUMI": [ + { + type: "TV", + title:"星期一", + subtype :"bangumi", + week :"1", + }, + { + type: "TV", + title:"星期二", + subtype :"bangumi", + week :"2", + }, + { + type: "TV", + title:"星期三", + subtype :"bangumi", + week :"3", + }, + { + type: "TV", + title:"星期四", + subtype :"bangumi", + week :"4", + }, + { + type: "TV", + title:"星期五", + subtype :"bangumi", + week :"5", + }, + { + type: "TV", + title:"星期六", + subtype :"bangumi", + week :"6", + }, + { + type: "TV", + title:"星期日", + subtype :"bangumi", + week :"7", + }, + ] + } + } + + firstUpdated() { + for (const item of this._media_type_list[this.discovery_type]) { + Golbal.get_cache_or_ajax( + "get_recommend", + self.discovery_type + item.title, + { "type": item.type, "subtype": item.subtype, "page": 1, "week": item.week}, + (ret) => { + this._slide_card_list = {...this._slide_card_list, [item.title]: ret.Items}; + } + ); + } + } + + render() { + return html` +
+ ${this._media_type_list[this.discovery_type]?.map((item) => ( html` + ( html` + { + Golbal.update_fav_data("get_recommend", item.subtype, (extra) => ( + extra.Items[index].fav = e.detail.fav, extra + )); + }} + lazy=1 + card-tmdbid=${card.id} + card-mediatype=${card.type} + card-showsub=1 + card-image=${card.image} + card-fav=${card.fav} + card-vote=${card.vote} + card-year=${card.year} + card-title=${card.title} + card-overview=${card.overview} + card-restype=${card.media_type} + class="px-2" + >`)) + : Array(20).fill(html``) + } + >` + ))} +
+ `; + } +} + + +window.customElements.define("page-discovery", PageDiscovery); \ No newline at end of file diff --git a/web/static/components/page/index.js b/web/static/components/page/index.js new file mode 100644 index 0000000..2f5f770 --- /dev/null +++ b/web/static/components/page/index.js @@ -0,0 +1,3 @@ +export * from "./discovery/index.js"; +export * from "./mediainfo/index.js"; +export * from "./person/index.js"; \ No newline at end of file diff --git a/web/static/components/page/mediainfo/index.js b/web/static/components/page/mediainfo/index.js new file mode 100644 index 0000000..77e72b6 --- /dev/null +++ b/web/static/components/page/mediainfo/index.js @@ -0,0 +1,305 @@ +import { html, nothing } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; + +export class PageMediainfo extends CustomElement { + static properties = { + // 类型 + media_type: { attribute: "media-type" }, + // TMDBID/DB:豆瓣ID + tmdbid: { attribute: "media-tmdbid" }, + // 是否订阅/下载 + fav: {}, + // 媒体信息 + media_info: { type: Object }, + // 类似影片 + similar_media: { type: Array }, + // 推荐影片 + recommend_media: { type: Array }, + }; + + constructor() { + super(); + this.media_info = {}; + this.similar_media = []; + this.recommend_media = []; + this.fav = undefined; + } + + firstUpdated() { + // 媒体信息、演员阵容 + Golbal.get_cache_or_ajax("media_detail", "info", { "type": this.media_type, "tmdbid": this.tmdbid}, + (ret) => { + if (ret.code === 0) { + this.media_info = ret.data; + this.tmdbid = ret.data.tmdbid; + this.fav = ret.data.fav; + // 类似 + Golbal.get_cache_or_ajax("get_recommend", "sim", { "type": this.media_type, "subtype": "sim", "tmdbid": ret.data.tmdbid, "page": 1}, + (ret) => { + if (ret.code === 0) { + this.similar_media = ret.Items; + } + } + ); + // 推荐 + Golbal.get_cache_or_ajax("get_recommend", "more", { "type": this.media_type, "subtype": "more", "tmdbid": ret.data.tmdbid, "page": 1}, + (ret) => { + if (ret.code === 0) { + this.recommend_media = ret.Items; + } + } + ); + } else { + show_fail_modal("未查询到TMDB媒体信息!"); + window.history.go(-1); + } + } + ); + } + + _render_placeholder(width, height, col, num) { + return Array(num ?? 1).fill(html` +
+
+ `); + } + + render() { + return html` + +
+ +
+ + +
+
+ + +
+
+ ${this.fav == "2" + ? html` +
+ 已下载 +
` + : nothing } +

+ ${this.media_info.title ?? this._render_placeholder("200px")} + (${this.media_info.year}) +

+
+ ${this.media_info.tmdbid} + ${this.media_info.douban_id} + ${this.media_info.runtime} + | ${this.media_info.genres} + ${Object.keys(this.media_info).length === 0 ? this._render_placeholder("205px") : nothing } +
+
+ ${Object.keys(this.media_info).length !== 0 + ? html` + { + e.stopPropagation(); + media_search(this.tmdbid + "", this.media_info.title, this.media_type); + }}> + + 搜索资源 + + ${this.fav == "1" + ? html` + + + 删除订阅 + ` + : html` + + + 添加订阅 + ` + }` + : html` + ${this._render_placeholder("100px", "30px")} + ${this._render_placeholder("100px", "30px")} + ` + } +
+
+
+
+

+ ${Object.keys(this.media_info).length === 0 ? "加载中.." : "简介"} +

+
+
+
+
+

+ ${this.media_info.overview ?? this._render_placeholder("200px", "", "col-12", 7)} +

+
+ ${this.media_info.crews + ? this.media_info.crews.map((item, index) => ( html` +
+

+ ${Object.keys(item)[0]} +

+

+ ${Object.values(item)[0]} +

+
+ `) ) + : nothing } +
+
+
+ ${this.media_info.fact + ? html` +
+
+ ${this.media_info.fact.map((item) => ( html` +
+
+
+ ${Object.keys(item)[0]} +
+
+ ${Object.values(item)[0]} +
+
+
+ `) ) } +
+
` + : this._render_placeholder("200px", "200px", "col-12") } +
+
+ + + ${this.media_info.actors && this.media_info.actors.length + ? html` + ( html` + { + navmenu("recommend?type="+this.media_type+"&subtype=person&personid="+item.id+"&title=参演作品&subtitle="+item.name) + }} + >`)) + } + >` + : nothing } + + + ${this.similar_media.length + ? html` + ( html` + { + Golbal.update_fav_data("get_recommend", "sim", (extra) => ( + extra.Items[index].fav = e.detail.fav, extra + )); + }} + lazy=1 + card-tmdbid=${item.id} + card-mediatype=${item.type} + card-showsub=1 + card-image=${item.image} + card-fav=${item.fav} + card-vote=${item.vote} + card-year=${item.year} + card-title=${item.title} + card-overview=${item.overview} + >`)) + } + >` + : nothing } + + + ${this.recommend_media.length + ? html` + ( html` + { + Golbal.update_fav_data("get_recommend", "more", (extra) => ( + extra.Items[index].fav = e.detail.fav, extra + )); + }} + lazy=1 + card-tmdbid=${item.id} + card-mediatype=${item.type} + card-showsub=1 + card-image=${item.image} + card-fav=${item.fav} + card-vote=${item.vote} + card-year=${item.year} + card-title=${item.title} + card-overview=${item.overview} + >`)) + } + >` + : nothing } + +
+ `; + } + + _update_fav_data() { + Golbal.update_fav_data("media_detail", "info", (extra) => ( + extra.data.fav = this.fav, extra + )); + } + + _loveClick(e) { + e.stopPropagation(); + Golbal.lit_love_click(this.media_info.title, this.media_info.year, this.media_type, this.tmdbid, this.fav, + () => { + this.fav = "0"; + this._update_fav_data(); + }, + () => { + this.fav = "1"; + this._update_fav_data(); + }); + } + + +} + + +window.customElements.define("page-mediainfo", PageMediainfo); \ No newline at end of file diff --git a/web/static/components/page/person/index.js b/web/static/components/page/person/index.js new file mode 100644 index 0000000..3abf5c3 --- /dev/null +++ b/web/static/components/page/person/index.js @@ -0,0 +1,67 @@ +import { html } from "../../utility/lit-core.min.js"; +import { CustomElement, Golbal } from "../../utility/utility.js"; + +export class PagePerson extends CustomElement { + static properties = { + page_title: { attribute: "page-title" }, + page_subtitle: { attribute: "page-subtitle"}, + media_type: { attribute: "media-type" }, + tmdbid: { attribute: "media-tmdbid" }, + person_list: { type: Array }, + }; + + constructor() { + super(); + this.person_list = []; + } + + // 仅执行一次 界面首次刷新后 + firstUpdated() { + Golbal.get_cache_or_ajax("media_person", this.media_type, { "tmdbid": this.tmdbid, "type": this.media_type}, + (ret) => { + if (ret.code === 0) { + this.person_list = ret.data; + } + } + ); + } + + render() { + return html` +
+ +
+
+
+
+ ${this.person_list.length != 0 + ? this.person_list.map((item, index) => ( html` + { + navmenu("recommend?type="+this.media_type+"&subtype=person&personid="+item.id+"&title=参演作品&subtitle="+item.name) + }} + > + ` ) ) + : Array(20).fill(html``) + } +
+
+
+ `; + } + +} + + +window.customElements.define("page-person", PagePerson); \ No newline at end of file diff --git a/web/static/components/utility/lit-core.min.js b/web/static/components/utility/lit-core.min.js new file mode 100644 index 0000000..dca0db4 --- /dev/null +++ b/web/static/components/utility/lit-core.min.js @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const t=window,i=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),e=new WeakMap;class o{constructor(t,i,e){if(this._$cssResult$=!0,e!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=i}get styleSheet(){let t=this.i;const s=this.t;if(i&&void 0===t){const i=void 0!==s&&1===s.length;i&&(t=e.get(s)),void 0===t&&((this.i=t=new CSSStyleSheet).replaceSync(this.cssText),i&&e.set(s,t))}return t}toString(){return this.cssText}}const n=t=>new o("string"==typeof t?t:t+"",void 0,s),r=(t,...i)=>{const e=1===t.length?t[0]:i.reduce(((i,s,e)=>i+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[e+1]),t[0]);return new o(e,t,s)},h=(s,e)=>{i?s.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((i=>{const e=document.createElement("style"),o=t.litNonce;void 0!==o&&e.setAttribute("nonce",o),e.textContent=i.cssText,s.appendChild(e)}))},l=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let i="";for(const s of t.cssRules)i+=s.cssText;return n(i)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var a;const u=window,c=u.trustedTypes,d=c?c.emptyScript:"",v=u.reactiveElementPolyfillSupport,p={toAttribute(t,i){switch(i){case Boolean:t=t?d:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t)}catch(t){s=null}}return s}},f=(t,i)=>i!==t&&(i==i||t==t),m={attribute:!0,type:String,converter:p,reflect:!1,hasChanged:f};class y extends HTMLElement{constructor(){super(),this.o=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this.l=null,this.u()}static addInitializer(t){var i;this.finalize(),(null!==(i=this.v)&&void 0!==i?i:this.v=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this.p(s,i);void 0!==e&&(this.m.set(e,s),t.push(e))})),t}static createProperty(t,i=m){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e)}}static getPropertyDescriptor(t,i,s){return{get(){return this[i]},set(e){const o=this[t];this[i]=e,this.requestUpdate(t,o,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||m}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.v&&(this.v=[...t.v]),this.elementProperties=new Map(t.elementProperties),this.m=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const i=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const t of s)i.unshift(l(t))}else void 0!==t&&i.push(l(t));return i}static p(t,i){const s=i.attribute;return!1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this.g(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var i,s;(null!==(i=this.S)&&void 0!==i?i:this.S=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t))}removeController(t){var i;null===(i=this.S)||void 0===i||i.splice(this.S.indexOf(t)>>>0,1)}g(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this.o.set(i,this[i]),delete this[i])}))}createRenderRoot(){var t;const i=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return h(i,this.constructor.elementStyles),i}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}))}attributeChangedCallback(t,i,s){this._$AK(t,s)}$(t,i,s=m){var e;const o=this.constructor.p(t,s);if(void 0!==o&&!0===s.reflect){const n=(void 0!==(null===(e=s.converter)||void 0===e?void 0:e.toAttribute)?s.converter:p).toAttribute(i,s.type);this.l=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.l=null}}_$AK(t,i){var s;const e=this.constructor,o=e.m.get(t);if(void 0!==o&&this.l!==o){const t=e.getPropertyOptions(o),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(s=t.converter)||void 0===s?void 0:s.fromAttribute)?t.converter:p;this.l=o,this[o]=n.fromAttribute(i,t.type),this.l=null}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||f)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this.l!==t&&(void 0===this.C&&(this.C=new Map),this.C.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._=this.T())}async T(){this.isUpdatePending=!0;try{await this._}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.o&&(this.o.forEach(((t,i)=>this[i]=t)),this.o=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this.P()}catch(t){throw i=!1,this.P(),t}i&&this._$AE(s)}willUpdate(t){}_$AE(t){var i;null===(i=this.S)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}P(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._}shouldUpdate(t){return!0}update(t){void 0!==this.C&&(this.C.forEach(((t,i)=>this.$(i,this[i],t))),this.C=void 0),this.P()}updated(t){}firstUpdated(t){}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var _;y.finalized=!0,y.elementProperties=new Map,y.elementStyles=[],y.shadowRootOptions={mode:"open"},null==v||v({ReactiveElement:y}),(null!==(a=u.reactiveElementVersions)&&void 0!==a?a:u.reactiveElementVersions=[]).push("1.5.0");const b=window,g=b.trustedTypes,w=g?g.createPolicy("lit-html",{createHTML:t=>t}):void 0,S=`lit$${(Math.random()+"").slice(9)}$`,$="?"+S,C=`<${$}>`,T=document,P=(t="")=>T.createComment(t),x=t=>null===t||"object"!=typeof t&&"function"!=typeof t,A=Array.isArray,k=t=>A(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),E=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,M=/-->/g,U=/>/g,N=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),R=/'/g,O=/"/g,V=/^(?:script|style|textarea|title)$/i,j=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),z=j(1),L=j(2),I=Symbol.for("lit-noChange"),H=Symbol.for("lit-nothing"),B=new WeakMap,D=T.createTreeWalker(T,129,null,!1),q=(t,i)=>{const s=t.length-1,e=[];let o,n=2===i?"":"",r=E;for(let i=0;i"===l[0]?(r=null!=o?o:E,a=-1):void 0===l[1]?a=-2:(a=r.lastIndex-l[2].length,h=l[1],r=void 0===l[3]?N:'"'===l[3]?O:R):r===O||r===R?r=N:r===M||r===U?r=E:(r=N,o=void 0);const c=r===N&&t[i+1].startsWith("/>")?" ":"";n+=r===E?s+C:a>=0?(e.push(h),s.slice(0,a)+"$lit$"+s.slice(a)+S+c):s+S+(-2===a?(e.push(void 0),i):c)}const h=n+(t[s]||"")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==w?w.createHTML(h):h,e]};class J{constructor({strings:t,_$litType$:i},s){let e;this.parts=[];let o=0,n=0;const r=t.length-1,h=this.parts,[l,a]=q(t,i);if(this.el=J.createElement(l,s),D.currentNode=this.el.content,2===i){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes)}for(;null!==(e=D.nextNode())&&h.length0){e.textContent=g?g.emptyScript:"";for(let s=0;s2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=H}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=W(this,t,i,0),n=!x(t)||t!==this._$AH&&t!==I,n&&(this._$AH=t);else{const e=t;let r,h;for(t=o[0],r=0;r{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let r=n._$litPart$;if(void 0===r){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=r=new F(i.insertBefore(P(),t),t,void 0,null!=s?s:{})}return r._$AI(t),r}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var ot,nt;const rt=y;class ht extends y{constructor(){super(...arguments),this.renderOptions={host:this},this.et=void 0}createRenderRoot(){var t,i;const s=super.createRenderRoot();return null!==(t=(i=this.renderOptions).renderBefore)&&void 0!==t||(i.renderBefore=s.firstChild),s}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this.et=et(i,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.et)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.et)||void 0===t||t.setConnected(!1)}render(){return I}}ht.finalized=!0,ht._$litElement$=!0,null===(ot=globalThis.litElementHydrateSupport)||void 0===ot||ot.call(globalThis,{LitElement:ht});const lt=globalThis.litElementPolyfillSupport;null==lt||lt({LitElement:ht});const at={_$AK:(t,i,s)=>{t._$AK(i,s)},_$AL:t=>t._$AL};(null!==(nt=globalThis.litElementVersions)&&void 0!==nt?nt:globalThis.litElementVersions=[]).push("3.2.2"); +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const ut=!1;export{o as CSSResult,ht as LitElement,y as ReactiveElement,rt as UpdatingElement,at as _$LE,it as _$LH,h as adoptStyles,r as css,p as defaultConverter,l as getCompatibleStyle,z as html,ut as isServer,I as noChange,f as notEqual,H as nothing,et as render,i as supportsAdoptingStyleSheets,L as svg,n as unsafeCSS}; +//# sourceMappingURL=lit-core.min.js.map diff --git a/web/static/components/utility/lit-state.js b/web/static/components/utility/lit-state.js new file mode 100644 index 0000000..b73c048 --- /dev/null +++ b/web/static/components/utility/lit-state.js @@ -0,0 +1,258 @@ +export const observeState = superclass => class extends superclass { + + constructor() { + super(); + this._observers = []; + } + + update(changedProperties) { + stateRecorder.start(); + super.update(changedProperties); + this._initStateObservers(); + } + + connectedCallback() { + super.connectedCallback(); + if (this._wasConnected) { + this.requestUpdate(); + delete this._wasConnected; + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._wasConnected = true; + this._clearStateObservers(); + } + + _initStateObservers() { + this._clearStateObservers(); + if (!this.isConnected) return; + this._addStateObservers(stateRecorder.finish()); + } + + _addStateObservers(stateVars) { + for (let [state, keys] of stateVars) { + const observer = () => this.requestUpdate(); + this._observers.push([state, observer]); + state.addObserver(observer, keys); + } + } + + _clearStateObservers() { + for (let [state, observer] of this._observers) { + state.removeObserver(observer); + } + this._observers = []; + } + +} + + +export class LitState { + + constructor() { + this._observers = []; + this._initStateVars(); + } + + addObserver(observer, keys) { + this._observers.push({observer, keys}); + } + + removeObserver(observer) { + this._observers = this._observers.filter(observerObj => observerObj.observer !== observer); + } + + _initStateVars() { + + if (this.constructor.stateVarOptions) { + for (let [key, options] of Object.entries(this.constructor.stateVarOptions)) { + this._initStateVar(key, options); + } + } + + if (this.constructor.stateVars) { + for (let [key, value] of Object.entries(this.constructor.stateVars)) { + this._initStateVar(key, {}); + this[key] = value; + } + } + + } + + _initStateVar(key, options) { + + if (this.hasOwnProperty(key)) { + // Property already defined, so don't re-define. + return; + } + + options = this._parseOptions(options); + + const stateVar = new options.handler({ + options: options, + recordRead: () => this._recordRead(key), + notifyChange: () => this._notifyChange(key) + }); + + Object.defineProperty( + this, + key, + { + get() { + return stateVar.get(); + }, + set(value) { + if (stateVar.shouldSetValue(value)) { + stateVar.set(value); + } + }, + configurable: true, + enumerable: true + } + ); + + } + + _parseOptions(options) { + + if (!options.handler) { + options.handler = StateVar; + } else { + + // In case of a custom `StateVar` handler is provided, we offer a + // second way of providing options to your custom handler class. + // + // You can decorate a *method* with `@stateVar()` instead of a + // variable. The method must return an object, and that object will + // be assigned to the `options` object. + // + // Within the method you have access to the `this` context. So you + // can access other properties and methods from your state class. + // And you can add arrow function callbacks where you can access + // `this`. This provides a lot of possibilities for a custom + // handler class. + if (options.propertyMethod && options.propertyMethod.kind === 'method') { + Object.assign(options, options.propertyMethod.descriptor.value.call(this)); + } + + } + + return options; + + } + + _recordRead(key) { + stateRecorder.recordRead(this, key); + } + + _notifyChange(key) { + for (const observerObj of this._observers) { + if (!observerObj.keys || observerObj.keys.includes(key)) { + observerObj.observer(key); + } + }; + } + +} + + +export class StateVar { + + constructor(args) { + this.options = args.options; // The options given in the `stateVar` declaration + this.recordRead = args.recordRead; // Callback to indicate the `stateVar` is read + this.notifyChange = args.notifyChange; // Callback to indicate the `stateVar` value has changed + this.value = undefined; // The initial value + } + + // Called when the `stateVar` on the `LitState` class is read (for example: + // `myState.myStateVar`). Should return the value of the `stateVar`. + get() { + this.recordRead(); + return this.value; + } + + // Called before the `set()` method is called. If this method returns + // `false`, the `set()` method won't be called. This can be used for + // validation and/or optimization. + shouldSetValue(value) { + return this.value !== value; + } + + // Called when the `stateVar` on the `LitState` class is set (for example: + // `myState.myStateVar = 'value'`. + set(value) { + this.value = value; + this.notifyChange(); + } + +} + + +export function stateVar(options = {}) { + + return element => { + + return { + kind: 'field', + key: Symbol(), + placement: 'own', + descriptor: {}, + initializer() { + if (typeof element.initializer === 'function') { + this[element.key] = element.initializer.call(this); + } + }, + finisher(litStateClass) { + + if (element.kind === 'method') { + // You can decorate a *method* with `@stateVar()` instead + // of a variable. When the state class is constructed, this + // method will be called, and it's return value must be an + // object that will be added to the options the stateVar + // handler will receive. + options.propertyMethod = element; + } + + if (litStateClass.stateVarOptions === undefined) { + litStateClass.stateVarOptions = {}; + } + + litStateClass.stateVarOptions[element.key] = options; + + } + }; + + }; + +} + + +class StateRecorder { + + constructor() { + this._log = null; + } + + start() { + this._log = new Map(); + } + + recordRead(stateObj, key) { + if (this._log === null) return; + const keys = this._log.get(stateObj) || []; + if (!keys.includes(key)) keys.push(key); + this._log.set(stateObj, keys); + } + + finish() { + const stateVars = this._log; + this._log = null; + return stateVars; + } + +} + +export const stateRecorder = new StateRecorder(); \ No newline at end of file diff --git a/web/static/components/utility/utility.js b/web/static/components/utility/utility.js new file mode 100644 index 0000000..0d84f4f --- /dev/null +++ b/web/static/components/utility/utility.js @@ -0,0 +1,122 @@ +import { LitElement } from "./lit-core.min.js"; + +export class CustomElement extends LitElement { + + // 兼容前进后退时重载 + connectedCallback() { + super.connectedCallback(); + this.innerHTML = ""; + } + + // 过滤空字符 + attributeChangedCallback(name, oldValue, newValue) { + super.attributeChangedCallback(name, oldValue, Golbal.repNull(newValue)); + } + + // 不使用影子dom + createRenderRoot() { + return this; + } + +} + +export class Golbal { + + // 没有图片时 + static noImage = "../static/img/no-image.png"; + static noImage_person = "../static/img/person.png"; + + // 转换传值的空字符情况 + static repNull(value) { + if (!value || value == "None" || value == "null" || value == "undefined") { + return ""; + } else { + return value; + } + } + + // 是否触摸屏设备 + static is_touch_device() { + return "ontouchstart" in window; + } + + static convert_mediaid(tmdbid) { + if (typeof(tmdbid) === "number") { + tmdbid = tmdbid + ""; + } + return tmdbid + } + + // 订阅按钮被点击时 + static lit_love_click(title, year, media_type, tmdb_id, fav, remove_func, add_func) { + if (fav == "1"){ + show_ask_modal("是否确定将 " + title + " 从订阅中移除?", function () { + hide_ask_modal(); + remove_rss_media(title, year, media_type, "", "", tmdb_id, remove_func); + }); + } else { + show_ask_modal("是否确定订阅: " + title + "?", function () { + hide_ask_modal(); + const mediaid = Golbal.convert_mediaid(tmdb_id); + if (media_type == "MOV" || media_type == "电影") { + add_rss_media(title, year, media_type, mediaid, "", "", add_func); + } else { + ajax_post("get_tvseason_list", {tmdbid: mediaid, title: title}, function (ret) { + if (ret.seasons.length === 1) { + add_rss_media(title, year, "TV", mediaid, "", ret.seasons[0].num, add_func); + } else if (ret.seasons.length > 1) { + show_rss_seasons_modal(title, year, "TV", mediaid, ret.seasons, add_func); + } else { + show_fail_modal(title + " 添加RSS订阅失败:未查询到季信息!"); + } + }); + } + }); + } + } + + // 保存额外的页面数据 + static save_page_data(name, value) { + const extra = window.history.state?.extra ?? {}; + extra[name] = value; + window_history(false, extra); + } + + // 获取额外的页面数据 + static get_page_data(name) { + return window.history.state?.extra ? window.history.state.extra[name] : undefined; + } + + // 判断直接获取缓存或ajax_post + static get_cache_or_ajax(api, name, data, func) { + const ret = Golbal.get_page_data(api + name); + //console.log("读取:", api + name, ret); + if (ret) { + func(ret); + } else { + const page = window.history.state?.page; + ajax_post(api, data, (ret) => { + // 页面已经变化, 丢弃该请求 + if (page !== window.history.state?.page) { + //console.log("丢弃:", api + name, ret); + return + } + Golbal.save_page_data(api + name, ret); + //console.log("缓存:", api + name, ret); + func(ret) + }); + } + } + + // 共用的fav数据更改时刷新缓存 + static update_fav_data(api, name, func=undefined) { + const key = api + name; + let extra = Golbal.get_page_data(key); + if (extra && func) { + extra = func(extra); + Golbal.save_page_data(key, extra); + //console.log("更新fav", extra); + } + } + +} \ No newline at end of file diff --git a/web/static/css/demo.min.css b/web/static/css/demo.min.css new file mode 100644 index 0000000..831780c --- /dev/null +++ b/web/static/css/demo.min.css @@ -0,0 +1,9 @@ +/*! +* Tabler v1.0.0-beta16 (https://tabler.io) +* @version 1.0.0-beta16 +* @link https://tabler.io +* Copyright 2018-2022 The Tabler Authors +* Copyright 2018-2022 codecalm.net Paweł Kuna +* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) +*/ +.highlight pre,pre.highlight{max-height:30rem;margin:1.5rem 0;overflow:auto;border-radius:var(--tblr-border-radius)}.highlight pre::-webkit-scrollbar,pre.highlight::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){.highlight pre::-webkit-scrollbar,pre.highlight::-webkit-scrollbar{-webkit-transition:none;transition:none}}.highlight pre::-webkit-scrollbar-thumb,pre.highlight::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-light-rgb),.16)}.highlight pre::-webkit-scrollbar-track,pre.highlight::-webkit-scrollbar-track{background:rgba(var(--tblr-light-rgb),.06)}.highlight pre:hover::-webkit-scrollbar-thumb,pre.highlight:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-light-rgb),.32)}.highlight pre::-webkit-scrollbar-corner,pre.highlight::-webkit-scrollbar-corner{background:0 0}.highlight{margin:0}.highlight code>*{margin:0!important;padding:0!important}.highlight .c,.highlight .c1{color:#a0aec0}.highlight .nc,.highlight .nt,.highlight .nx{color:#ff8383}.highlight .na,.highlight .p{color:#ffe484}.highlight .dl,.highlight .s,.highlight .s2{color:#b5f4a5}.highlight .k{color:#93ddfd}.highlight .mi,.highlight .s1{color:#d9a9ff}.example{padding:2rem;margin:1rem 0 2rem;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:3px 3px 0 0;position:relative;min-height:12rem;display:flex;align-items:center;overflow-x:auto}.example-centered{justify-content:center}.example-centered .example-content{flex:0 auto}.example-content{font-size:.875rem;line-height:1.4285714286;color:#1d273b;flex:1;max-width:100%}.example-content .page-header{margin-bottom:0}.example-bg{background:#f1f5f9}.example-code{margin:2rem 0;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-top:none}.example-code pre{margin:0;border:0;border-radius:0 0 3px 3px}.example+.example-code{margin-top:-2rem}.example-column{margin:0 auto}.example-column>.card:last-of-type{margin-bottom:0}.example-column-1{max-width:26rem}.example-column-2{max-width:52rem}.example-modal-backdrop{background:#1d273b;opacity:.24;position:absolute;width:100%;left:0;top:0;height:100%;border-radius:2px 2px 0 0}@media not print{.theme-dark .example{background-color:#1a2234;border-color:#243049}.theme-dark .example-content{color:#f8fafc}.theme-dark .example-code{border-color:#243049;border-top:none}}@media not print{@media (prefers-color-scheme:dark){.theme-dark-auto .example{background-color:#1a2234;border-color:#243049}.theme-dark-auto .example-content{color:#f8fafc}.theme-dark-auto .example-code{border-color:#243049;border-top:none}}}.card-sponsor{background:#dbe7f6 no-repeat center/100% 100%;border-color:#548ed2;min-height:316px}body.no-transitions *{transition:none!important}.dropdown-menu-demo{display:inline-block;width:100%;position:relative;top:0;margin-bottom:1rem!important}.demo-icon-preview{position:-webkit-sticky;position:sticky;top:0}.demo-icon-preview i,.demo-icon-preview svg{width:15rem;height:15rem;font-size:15rem;stroke-width:1.5;margin:0 auto;display:block}@media (max-width:575.98px){.demo-icon-preview i,.demo-icon-preview svg{width:10rem;height:10rem;font-size:10rem}}.demo-icon-preview-icon pre{margin:0;-webkit-user-select:all;-moz-user-select:all;user-select:all}.demo-dividers>p{opacity:.2;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.demo-icons-list{display:flex;flex-wrap:wrap;padding:0;margin:0 -2px -1px 0;list-style:none}.demo-icons-list>*{flex:1 0 4rem}.demo-icons-list-wrap{overflow:hidden}.demo-icons-list-item{display:flex;flex-direction:column;align-items:center;justify-content:center;aspect-ratio:1;text-align:center;padding:.5rem;border-right:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);color:inherit;cursor:pointer}.demo-icons-list-item .icon{width:1.5rem;height:1.5rem;font-size:1.5rem}.demo-icons-list-item:hover{text-decoration:none}.settings-btn{position:fixed;right:-1px;top:10rem;border-top-right-radius:0;border-bottom-right-radius:0;box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px 0}.settings-scheme{display:inline-block;border-radius:50%;height:3rem;width:3rem;position:relative;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px 0}.settings-scheme-light{background:linear-gradient(135deg,#fff 50%,#f8fafc 50%)}.settings-scheme-mixed{background-image:linear-gradient(135deg,#1d273b 50%,#fff 50%)}.settings-scheme-transparent{background:#f8fafc}.settings-scheme-dark{background:#1d273b}.settings-scheme-colored{background-image:linear-gradient(135deg,var(--tblr-primary) 50%,#f8fafc 50%)} \ No newline at end of file diff --git a/web/static/css/dropzone.css b/web/static/css/dropzone.css new file mode 100644 index 0000000..569ac44 --- /dev/null +++ b/web/static/css/dropzone.css @@ -0,0 +1 @@ +@keyframes passing-through{0%{opacity:0;transform:translateY(40px)}30%,70%{opacity:1;transform:translateY(0px)}100%{opacity:0;transform:translateY(-40px)}}@keyframes slide-in{0%{opacity:0;transform:translateY(40px)}30%{opacity:1;transform:translateY(0px)}}@keyframes pulse{0%{transform:scale(1)}10%{transform:scale(1.1)}20%{transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:1px solid rgba(0,0,0,.8);border-radius:5px;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:.5}.dropzone .dz-message{text-align:center;margin:3em 0}.dropzone .dz-message .dz-button{background:none;color:inherit;border:none;padding:0;font:inherit;cursor:pointer;outline:inherit}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:#fff}.dropzone .dz-preview.dz-image-preview .dz-details{transition:opacity .2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,.8);background-color:rgba(255,255,255,.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,.4);padding:0 .4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{transform:scale(1.05, 1.05);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px;background:rgba(0,0,0,.8);border-radius:50%}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px;fill:#fff}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;transition:all .2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;transition:opacity .4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:20px;top:50%;margin-top:-10px;left:15%;right:15%;border:3px solid rgba(0,0,0,.8);background:rgba(0,0,0,.8);border-radius:10px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#fff;display:block;position:relative;height:100%;width:0;transition:width 300ms ease-in-out;border-radius:17px}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;transition:opacity .3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#b10606;padding:.5em 1em;color:#fff}.dropzone .dz-preview .dz-error-message:after{content:"";position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #b10606}/*# sourceMappingURL=dropzone.css.map */ diff --git a/web/static/css/fullcalendar.min.css b/web/static/css/fullcalendar.min.css new file mode 100644 index 0000000..5c0056e --- /dev/null +++ b/web/static/css/fullcalendar.min.css @@ -0,0 +1 @@ +.fc-icon,.fc-unselectable{-moz-user-select:none;-ms-user-select:none}.fc .fc-button,.fc-icon{text-transform:none;font-weight:400}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc .fc-button:not(:disabled),.fc a[data-navlink],.fc-event.fc-event-draggable,.fc-event[href]{cursor:pointer}.fc-unselectable{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.fc{display:flex;flex-direction:column;font-size:1em}.fc .fc-button,.fc-icon{display:inline-block;text-align:center}.fc,.fc *,.fc :after,.fc :before{box-sizing:border-box}.fc table{border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{vertical-align:top;padding:0}.fc .fc-button,.fc .fc-button .fc-icon,.fc .fc-button-group,.fc .fc-timegrid-slot-label{vertical-align:middle}.fc a[data-navlink]:hover{text-decoration:underline}.fc .fc-button:hover,.fc .fc-list-event-title a,a.fc-event,a.fc-event:hover{text-decoration:none}.fc-direction-ltr{direction:ltr;text-align:left}.fc-direction-rtl{direction:rtl;text-align:right}.fc-theme-standard td,.fc-theme-standard th{border:1px solid #ddd;border:1px solid var(--fc-border-color,#ddd)}.fc-liquid-hack td,.fc-liquid-hack th{position:relative}@font-face{font-family:fcicons;src:url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format('truetype');font-weight:400;font-style:normal}.fc-icon{width:1em;height:1em;-webkit-user-select:none;user-select:none;font-family:fcicons!important;speak:none;font-style:normal;font-variant:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fc-icon-chevron-left:before{content:"\e900"}.fc-icon-chevron-right:before{content:"\e901"}.fc-icon-chevrons-left:before{content:"\e902"}.fc-icon-chevrons-right:before{content:"\e903"}.fc-icon-minus-square:before{content:"\e904"}.fc-icon-plus-square:before{content:"\e905"}.fc-icon-x:before{content:"\e906"}.fc .fc-button{overflow:visible;text-transform:none;margin:0;font-family:inherit}.fc .fc-button::-moz-focus-inner{padding:0;border-style:none}.fc .fc-button{-webkit-appearance:button;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.4em .65em;font-size:1em;line-height:1.5;border-radius:.25em}.fc .fc-button:focus{outline:0;box-shadow:0 0 0 .2rem rgba(44,62,80,.25)}.fc .fc-button-primary:focus,.fc .fc-button-primary:not(:disabled).fc-button-active:focus,.fc .fc-button-primary:not(:disabled):active:focus{box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc .fc-button:disabled{opacity:.65}.fc .fc-button-primary{color:#fff;color:var(--fc-button-text-color,#fff);background-color:#2C3E50;background-color:var(--fc-button-bg-color,#2C3E50);border-color:#2C3E50;border-color:var(--fc-button-border-color,#2C3E50)}.fc .fc-button-primary:hover{color:#fff;color:var(--fc-button-text-color,#fff);background-color:#1e2b37;background-color:var(--fc-button-hover-bg-color,#1e2b37);border-color:#1a252f;border-color:var(--fc-button-hover-border-color,#1a252f)}.fc .fc-button-primary:disabled{color:#fff;color:var(--fc-button-text-color,#fff);background-color:#2C3E50;background-color:var(--fc-button-bg-color,#2C3E50);border-color:#2C3E50;border-color:var(--fc-button-border-color,#2C3E50)}.fc .fc-button-primary:not(:disabled).fc-button-active,.fc .fc-button-primary:not(:disabled):active{color:#fff;color:var(--fc-button-text-color,#fff);background-color:#1a252f;background-color:var(--fc-button-active-bg-color,#1a252f);border-color:#151e27;border-color:var(--fc-button-active-border-color,#151e27)}.fc .fc-button .fc-icon{font-size:1.5em}.fc .fc-button-group{position:relative;display:inline-flex}.fc .fc-button-group>.fc-button{position:relative;flex:1 1 auto}.fc .fc-button-group>.fc-button.fc-button-active,.fc .fc-button-group>.fc-button:active,.fc .fc-button-group>.fc-button:focus,.fc .fc-button-group>.fc-button:hover{z-index:1}.fc-direction-ltr .fc-button-group>.fc-button:not(:first-child){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-direction-ltr .fc-button-group>.fc-button:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.fc-direction-rtl .fc-button-group>.fc-button:not(:first-child){margin-right:-1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-direction-rtl .fc-button-group>.fc-button:not(:last-child){border-top-left-radius:0;border-bottom-left-radius:0}.fc .fc-toolbar{display:flex;justify-content:space-between;align-items:center}.fc .fc-toolbar.fc-header-toolbar{margin-bottom:1.5em}.fc .fc-toolbar.fc-footer-toolbar{margin-top:1.5em}.fc .fc-toolbar-title{font-size:1.75em;margin:0}.fc-direction-ltr .fc-toolbar>*>:not(:first-child){margin-left:.75em}.fc-direction-rtl .fc-toolbar>*>:not(:first-child){margin-right:.75em}.fc-direction-rtl .fc-toolbar-ltr{flex-direction:row-reverse}.fc .fc-scroller{-webkit-overflow-scrolling:touch;position:relative}.fc .fc-scroller-liquid{height:100%}.fc .fc-scroller-liquid-absolute{position:absolute;top:0;right:0;left:0;bottom:0}.fc .fc-scroller-harness{position:relative;overflow:hidden;direction:ltr}.fc .fc-scroller-harness-liquid{height:100%}.fc-direction-rtl .fc-scroller-harness>.fc-scroller{direction:rtl}.fc-theme-standard .fc-scrollgrid{border:1px solid #ddd;border:1px solid var(--fc-border-color,#ddd)}.fc .fc-scrollgrid,.fc .fc-scrollgrid-section-footer>*,.fc .fc-scrollgrid-section-header>*{border-bottom-width:0}.fc .fc-scrollgrid,.fc .fc-scrollgrid table{width:100%;table-layout:fixed}.fc .fc-scrollgrid table{border-top-style:hidden;border-left-style:hidden;border-right-style:hidden}.fc .fc-scrollgrid{border-collapse:separate;border-right-width:0}.fc .fc-scrollgrid-liquid{height:100%}.fc .fc-scrollgrid-section,.fc .fc-scrollgrid-section table,.fc .fc-scrollgrid-section>td{height:1px}.fc .fc-scrollgrid-section-liquid>td{height:100%}.fc .fc-scrollgrid-section>*{border-top-width:0;border-left-width:0}.fc .fc-scrollgrid-section-body table,.fc .fc-scrollgrid-section-footer table{border-bottom-style:hidden}.fc .fc-scrollgrid-section-sticky>*{background:var(--fc-page-bg-color,#fff);position:sticky;z-index:3}.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky>*{top:0}.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky>*{bottom:0}.fc .fc-scrollgrid-sticky-shim{height:1px;margin-bottom:-1px}.fc-sticky{position:sticky}.fc .fc-view-harness{flex-grow:1;position:relative}.fc .fc-bg-event,.fc .fc-highlight,.fc .fc-non-business,.fc .fc-view-harness-active>.fc-view{position:absolute;top:0;left:0;right:0;bottom:0}.fc .fc-col-header-cell-cushion{display:inline-block;padding:2px 4px}.fc .fc-non-business{background:rgba(215,215,215,.3);background:var(--fc-non-business-color,rgba(215,215,215,.3))}.fc .fc-bg-event{background:var(--fc-bg-event-color,#8fdf82);opacity:.3;opacity:var(--fc-bg-event-opacity,.3)}.fc .fc-bg-event .fc-event-title{margin:.5em;font-size:.85em;font-size:var(--fc-small-font-size,.85em);font-style:italic}.fc .fc-highlight{background:rgba(188,232,241,.3);background:var(--fc-highlight-color,rgba(188,232,241,.3))}.fc .fc-cell-shaded,.fc .fc-day-disabled{background:rgba(208,208,208,.3);background:var(--fc-neutral-bg-color,rgba(208,208,208,.3))}.fc-event .fc-event-main{position:relative;z-index:2}.fc-event-dragging:not(.fc-event-selected){opacity:.75}.fc-event-dragging.fc-event-selected{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-event .fc-event-resizer{display:none;position:absolute;z-index:4}.fc-event-selected .fc-event-resizer,.fc-event:hover .fc-event-resizer,.fc-h-event,.fc-v-event{display:block}.fc-event-selected .fc-event-resizer{border-radius:4px;border-radius:calc(var(--fc-event-resizer-dot-total-width,8px)/ 2);border-width:1px;border-width:var(--fc-event-resizer-dot-border-width,1px);width:8px;width:var(--fc-event-resizer-dot-total-width,8px);height:8px;height:var(--fc-event-resizer-dot-total-width,8px);border-style:solid;border-color:inherit;background:var(--fc-page-bg-color,#fff)}.fc-event-selected .fc-event-resizer:before{content:'';position:absolute;top:-20px;left:-20px;right:-20px;bottom:-20px}.fc-event-selected,.fc-event:focus{box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event-selected:before,.fc-event:focus:before{content:"";position:absolute;z-index:3;top:0;left:0;right:0;bottom:0}.fc-event-selected:after,.fc-event:focus:after{content:"";background:rgba(0,0,0,.25);background:var(--fc-event-selected-overlay-color,rgba(0,0,0,.25));position:absolute;z-index:1;top:-1px;left:-1px;right:-1px;bottom:-1px}.fc-h-event{border:1px solid #3788d8;border:1px solid var(--fc-event-border-color,#3788d8);background-color:#3788d8;background-color:var(--fc-event-bg-color,#3788d8)}.fc-h-event .fc-event-main{color:#fff;color:var(--fc-event-text-color,#fff)}.fc-h-event .fc-event-main-frame{display:flex}.fc-h-event .fc-event-time{max-width:100%;overflow:hidden}.fc-h-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-width:0}.fc-h-event .fc-event-title{display:inline-block;vertical-align:top;left:0;right:0;max-width:100%;overflow:hidden}.fc-h-event.fc-event-selected:before{top:-10px;bottom:-10px}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end){border-top-left-radius:0;border-bottom-left-radius:0;border-left-width:0}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start){border-top-right-radius:0;border-bottom-right-radius:0;border-right-width:0}.fc-h-event:not(.fc-event-selected) .fc-event-resizer{top:0;bottom:0;width:8px;width:var(--fc-event-resizer-thickness,8px)}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end{cursor:w-resize;left:-4px;left:calc(-.5 * var(--fc-event-resizer-thickness,8px))}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start{cursor:e-resize;right:-4px;right:calc(-.5 * var(--fc-event-resizer-thickness,8px))}.fc-h-event.fc-event-selected .fc-event-resizer{top:50%;margin-top:-4px;margin-top:calc(-.5 * var(--fc-event-resizer-dot-total-width,8px))}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end{left:-4px;left:calc(-.5 * var(--fc-event-resizer-dot-total-width,8px))}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start{right:-4px;right:calc(-.5 * var(--fc-event-resizer-dot-total-width,8px))}.fc .fc-popover{position:absolute;z-index:9999;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc .fc-popover-header{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:3px 4px}.fc .fc-popover-title{margin:0 2px}.fc .fc-popover-close{cursor:pointer;opacity:.65;font-size:1.1em}.fc-theme-standard .fc-popover{border:1px solid #ddd;border:1px solid var(--fc-border-color,#ddd);background:var(--fc-page-bg-color,#fff)}.fc-theme-standard .fc-popover-header{background:rgba(208,208,208,.3);background:var(--fc-neutral-bg-color,rgba(208,208,208,.3))}:root{--fc-daygrid-event-dot-width:8px;--fc-list-event-dot-width:10px;--fc-list-event-hover-bg-color:#f5f5f5}.fc-daygrid-day-events:after,.fc-daygrid-day-events:before,.fc-daygrid-day-frame:after,.fc-daygrid-day-frame:before,.fc-daygrid-event-harness:after,.fc-daygrid-event-harness:before{content:"";clear:both;display:table}.fc .fc-daygrid-body{position:relative;z-index:1}.fc .fc-daygrid-day.fc-day-today{background-color:rgba(255,220,40,.15);background-color:var(--fc-today-bg-color,rgba(255,220,40,.15))}.fc .fc-daygrid-day-frame{position:relative;min-height:100%}.fc .fc-daygrid-day-top{display:flex;flex-direction:row-reverse}.fc .fc-day-other .fc-daygrid-day-top{opacity:.3}.fc .fc-daygrid-day-number{position:relative;z-index:4;padding:4px}.fc .fc-daygrid-day-events{margin-top:1px}.fc .fc-daygrid-body-balanced .fc-daygrid-day-events{position:absolute;left:0;right:0}.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events{position:relative;min-height:2em}.fc .fc-daygrid-body-natural .fc-daygrid-day-events{margin-bottom:1em}.fc .fc-daygrid-event-harness{position:relative}.fc .fc-daygrid-event-harness-abs{position:absolute;top:0;left:0;right:0}.fc .fc-daygrid-bg-harness{position:absolute;top:0;bottom:0}.fc .fc-daygrid-day-bg .fc-non-business{z-index:1}.fc .fc-daygrid-day-bg .fc-bg-event{z-index:2}.fc .fc-daygrid-day-bg .fc-highlight{z-index:3}.fc .fc-daygrid-event{z-index:6;margin-top:1px}.fc .fc-daygrid-event.fc-event-mirror{z-index:7}.fc .fc-daygrid-day-bottom{font-size:.85em;padding:2px 3px 0}.fc .fc-daygrid-day-bottom:before{content:"";clear:both;display:table}.fc .fc-daygrid-more-link{position:relative;z-index:4;cursor:pointer}.fc .fc-daygrid-week-number{position:absolute;z-index:5;top:0;padding:2px;min-width:1.5em;text-align:center;background-color:rgba(208,208,208,.3);background-color:var(--fc-neutral-bg-color,rgba(208,208,208,.3));color:grey;color:var(--fc-neutral-text-color,grey)}.fc .fc-more-popover .fc-popover-body{min-width:220px;padding:10px}.fc-direction-ltr .fc-daygrid-event.fc-event-start,.fc-direction-rtl .fc-daygrid-event.fc-event-end{margin-left:2px}.fc-direction-ltr .fc-daygrid-event.fc-event-end,.fc-direction-rtl .fc-daygrid-event.fc-event-start{margin-right:2px}.fc-direction-ltr .fc-daygrid-week-number{left:0;border-radius:0 0 3px}.fc-direction-rtl .fc-daygrid-week-number{right:0;border-radius:0 0 0 3px}.fc-liquid-hack .fc-daygrid-day-frame{position:static}.fc-daygrid-event{position:relative;white-space:nowrap;border-radius:3px;font-size:.85em;font-size:var(--fc-small-font-size,.85em)}.fc-daygrid-block-event .fc-event-time{font-weight:700}.fc-daygrid-block-event .fc-event-time,.fc-daygrid-block-event .fc-event-title{padding:1px}.fc-daygrid-dot-event{display:flex;align-items:center;padding:2px 0}.fc-daygrid-dot-event .fc-event-title{flex-grow:1;flex-shrink:1;min-width:0;overflow:hidden;font-weight:700}.fc-daygrid-dot-event.fc-event-mirror,.fc-daygrid-dot-event:hover{background:rgba(0,0,0,.1)}.fc-daygrid-dot-event.fc-event-selected:before{top:-10px;bottom:-10px}.fc-daygrid-event-dot{margin:0 4px;box-sizing:content-box;width:0;height:0;border:4px solid #3788d8;border:calc(var(--fc-daygrid-event-dot-width,8px)/ 2) solid var(--fc-event-border-color,#3788d8);border-radius:4px;border-radius:calc(var(--fc-daygrid-event-dot-width,8px)/ 2)}.fc-direction-ltr .fc-daygrid-event .fc-event-time{margin-right:3px}.fc-direction-rtl .fc-daygrid-event .fc-event-time{margin-left:3px}.fc-v-event{border:1px solid #3788d8;border:1px solid var(--fc-event-border-color,#3788d8);background-color:#3788d8;background-color:var(--fc-event-bg-color,#3788d8)}.fc-v-event .fc-event-main{color:#fff;color:var(--fc-event-text-color,#fff);height:100%}.fc-v-event .fc-event-main-frame{height:100%;display:flex;flex-direction:column}.fc-v-event .fc-event-time{flex-grow:0;flex-shrink:0;max-height:100%;overflow:hidden}.fc-v-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-height:0}.fc-v-event .fc-event-title{top:0;bottom:0;max-height:100%;overflow:hidden}.fc-v-event:not(.fc-event-start){border-top-width:0;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event:not(.fc-event-end){border-bottom-width:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-v-event.fc-event-selected:before{left:-10px;right:-10px}.fc-v-event .fc-event-resizer-start{cursor:n-resize}.fc-v-event .fc-event-resizer-end{cursor:s-resize}.fc-v-event:not(.fc-event-selected) .fc-event-resizer{height:8px;height:var(--fc-event-resizer-thickness,8px);left:0;right:0}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start{top:-4px;top:calc(var(--fc-event-resizer-thickness,8px)/ -2)}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end{bottom:-4px;bottom:calc(var(--fc-event-resizer-thickness,8px)/ -2)}.fc-v-event.fc-event-selected .fc-event-resizer{left:50%;margin-left:-4px;margin-left:calc(var(--fc-event-resizer-dot-total-width,8px)/ -2)}.fc-v-event.fc-event-selected .fc-event-resizer-start{top:-4px;top:calc(var(--fc-event-resizer-dot-total-width,8px)/ -2)}.fc-v-event.fc-event-selected .fc-event-resizer-end{bottom:-4px;bottom:calc(var(--fc-event-resizer-dot-total-width,8px)/ -2)}.fc .fc-timegrid .fc-daygrid-body{z-index:2}.fc .fc-timegrid-axis-chunk>table,.fc .fc-timegrid-body,.fc .fc-timegrid-slots{position:relative;z-index:1}.fc .fc-timegrid-divider{padding:0 0 2px}.fc .fc-timegrid-body{min-height:100%}.fc .fc-timegrid-axis-chunk{position:relative}.fc .fc-timegrid-slot{height:1.5em;border-bottom:0}.fc .fc-timegrid-slot:empty:before{content:'\00a0'}.fc .fc-timegrid-slot-minor{border-top-style:dotted}.fc .fc-timegrid-slot-label-cushion{display:inline-block;white-space:nowrap}.fc .fc-timegrid-axis-cushion,.fc .fc-timegrid-slot-label-cushion{padding:0 4px}.fc .fc-timegrid-axis-frame-liquid{height:100%}.fc .fc-timegrid-axis-frame{overflow:hidden;display:flex;align-items:center;justify-content:flex-end}.fc .fc-timegrid-axis-cushion{max-width:60px;flex-shrink:0}.fc-direction-ltr .fc-timegrid-slot-label-frame{text-align:right}.fc-direction-rtl .fc-timegrid-slot-label-frame{text-align:left}.fc-liquid-hack .fc-timegrid-axis-frame-liquid{height:auto;position:absolute;top:0;right:0;bottom:0;left:0}.fc .fc-timegrid-col.fc-day-today{background-color:rgba(255,220,40,.15);background-color:var(--fc-today-bg-color,rgba(255,220,40,.15))}.fc .fc-timegrid-col-frame{min-height:100%;position:relative}.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame{height:auto;position:absolute;top:0;right:0;bottom:0;left:0}.fc-media-screen .fc-timegrid-cols{position:absolute;top:0;left:0;right:0;bottom:0}.fc-media-screen .fc-timegrid-cols>table{height:100%}.fc-media-screen .fc-timegrid-col-bg,.fc-media-screen .fc-timegrid-col-events,.fc-media-screen .fc-timegrid-now-indicator-container{position:absolute;top:0;left:0;right:0}.fc .fc-timegrid-col-bg{z-index:2}.fc .fc-timegrid-col-bg .fc-non-business{z-index:1}.fc .fc-timegrid-col-bg .fc-bg-event{z-index:2}.fc .fc-timegrid-col-bg .fc-highlight,.fc .fc-timegrid-col-events{z-index:3}.fc .fc-timegrid-bg-harness{position:absolute;left:0;right:0}.fc .fc-timegrid-now-indicator-container{bottom:0;overflow:hidden}.fc-direction-ltr .fc-timegrid-col-events{margin:0 2.5% 0 2px}.fc-direction-rtl .fc-timegrid-col-events{margin:0 2px 0 2.5%}.fc-timegrid-event-harness{position:absolute}.fc-timegrid-event-harness>.fc-timegrid-event{position:absolute;top:0;bottom:0;left:0;right:0}.fc-timegrid-event-harness-inset .fc-timegrid-event,.fc-timegrid-event.fc-event-mirror,.fc-timegrid-more-link{box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px var(--fc-page-bg-color,#fff)}.fc-timegrid-event,.fc-timegrid-more-link{font-size:.85em;font-size:var(--fc-small-font-size,.85em);border-radius:3px}.fc-timegrid-event{margin-bottom:1px}.fc-timegrid-event .fc-event-main{padding:1px 1px 0}.fc-timegrid-event .fc-event-time{white-space:nowrap;font-size:.85em;font-size:var(--fc-small-font-size,.85em);margin-bottom:1px}.fc-timegrid-event-short .fc-event-main-frame{flex-direction:row;overflow:hidden}.fc-timegrid-event-short .fc-event-time:after{content:'\00a0-\00a0'}.fc-timegrid-event-short .fc-event-title{font-size:.85em;font-size:var(--fc-small-font-size,.85em)}.fc-timegrid-more-link{position:absolute;z-index:9999;color:inherit;color:var(--fc-more-link-text-color,inherit);background:var(--fc-more-link-bg-color,#d0d0d0);cursor:pointer;margin-bottom:1px}.fc-timegrid-more-link-inner{padding:3px 2px;top:0}.fc-direction-ltr .fc-timegrid-more-link{right:0}.fc-direction-rtl .fc-timegrid-more-link{left:0}.fc .fc-timegrid-now-indicator-line{position:absolute;z-index:4;left:0;right:0;border-style:solid;border-color:red;border-color:var(--fc-now-indicator-color,red);border-width:1px 0 0}.fc .fc-timegrid-now-indicator-arrow{position:absolute;z-index:4;margin-top:-5px;border-style:solid;border-color:red;border-color:var(--fc-now-indicator-color,red)}.fc-direction-ltr .fc-timegrid-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-direction-rtl .fc-timegrid-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}.fc-theme-standard .fc-list{border:1px solid #ddd;border:1px solid var(--fc-border-color,#ddd)}.fc .fc-list-empty{background-color:rgba(208,208,208,.3);background-color:var(--fc-neutral-bg-color,rgba(208,208,208,.3));height:100%;display:flex;justify-content:center;align-items:center}.fc .fc-list-empty-cushion{margin:5em 0}.fc .fc-list-table{width:100%;border-style:hidden}.fc .fc-list-table tr>*{border-left:0;border-right:0}.fc .fc-list-sticky .fc-list-day>*{position:sticky;top:0;background:var(--fc-page-bg-color,#fff)}.fc .fc-list-table thead{position:absolute;left:-10000px}.fc .fc-list-table tbody>tr:first-child th{border-top:0}.fc .fc-list-table th{padding:0}.fc .fc-list-day-cushion,.fc .fc-list-table td{padding:8px 14px}.fc .fc-list-day-cushion:after{content:"";clear:both;display:table}.fc-theme-standard .fc-list-day-cushion{background-color:rgba(208,208,208,.3);background-color:var(--fc-neutral-bg-color,rgba(208,208,208,.3))}.fc-direction-ltr .fc-list-day-text,.fc-direction-rtl .fc-list-day-side-text{float:left}.fc-direction-ltr .fc-list-day-side-text,.fc-direction-rtl .fc-list-day-text{float:right}.fc-direction-ltr .fc-list-table .fc-list-event-graphic{padding-right:0}.fc-direction-rtl .fc-list-table .fc-list-event-graphic{padding-left:0}.fc .fc-list-event.fc-event-forced-url{cursor:pointer}.fc .fc-list-event:hover td{background-color:#f5f5f5;background-color:var(--fc-list-event-hover-bg-color,#f5f5f5)}.fc .fc-list-event-graphic,.fc .fc-list-event-time{white-space:nowrap;width:1px}.fc .fc-list-event-dot{display:inline-block;box-sizing:content-box;width:0;height:0;border:5px solid #3788d8;border:calc(var(--fc-list-event-dot-width,10px)/ 2) solid var(--fc-event-border-color,#3788d8);border-radius:5px;border-radius:calc(var(--fc-list-event-dot-width,10px)/ 2)}.fc .fc-list-event-title a{color:inherit}.fc .fc-list-event.fc-event-forced-url:hover a{text-decoration:underline}.fc-theme-bootstrap a:not([href]){color:inherit}.fc-theme-bootstrap5 a:not([href]){color:inherit;text-decoration:inherit}.fc-theme-bootstrap5 .fc-list,.fc-theme-bootstrap5 .fc-scrollgrid,.fc-theme-bootstrap5 td,.fc-theme-bootstrap5 th{border:1px solid var(--bs-gray-400)}.fc-theme-bootstrap5 .fc-scrollgrid{border-right-width:0;border-bottom-width:0}.fc-theme-bootstrap5-shaded{background-color:var(--bs-gray-200)} \ No newline at end of file diff --git a/web/static/css/jquery.filetree.css b/web/static/css/jquery.filetree.css new file mode 100644 index 0000000..ceb3931 --- /dev/null +++ b/web/static/css/jquery.filetree.css @@ -0,0 +1,74 @@ +UL.jqueryFileTree{padding:0px;margin:0px;display:none;line-height:150%;} +UL.jqueryFileTree LI{list-style:none;padding:0px;padding-left:20px;margin:0px;white-space:nowrap;} +UL.jqueryFileTree A{color:inherit;text-decoration:none;display:inline-block;padding:0px 2px;cursor:pointer;} +UL.jqueryFileTree A:hover{background:#90b5e2;} +/* Core Styles */ +.jqueryFileTree LI.directory{background:url(../img/filetree/directory.png) left top no-repeat;white-space:normal!important;word-break:break-all;} +.jqueryFileTree LI.expanded{background:url(../img/filetree/folder_open.png) left top no-repeat;} +.jqueryFileTree LI.expanded > a{font-weight: bold;} +.jqueryFileTree LI.file{background:url(../img/filetree/file.png) left top no-repeat;} +.jqueryFileTree LI.wait{background:url(../img/filetree/spinner.gif) left top no-repeat;} +/* File Extensions*/ +.jqueryFileTree LI.ext_3gp{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_afp{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_afpa{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_asp{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_aspx{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_avi{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_bat{background:url(../img/filetree/application.png) left top no-repeat;} +.jqueryFileTree LI.ext_bmp{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_c{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_cfm{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_cgi{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_com{background:url(../img/filetree/application.png) left top no-repeat;} +.jqueryFileTree LI.ext_cpp{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_css{background:url(../img/filetree/css.png) left top no-repeat;} +.jqueryFileTree LI.ext_doc{background:url(../img/filetree/doc.png) left top no-repeat;} +.jqueryFileTree LI.ext_exe{background:url(../img/filetree/application.png) left top no-repeat;} +.jqueryFileTree LI.ext_gif{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_fla{background:url(../img/filetree/swf.png) left top no-repeat;} +.jqueryFileTree LI.ext_h{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_htm{background:url(../img/filetree/html.png) left top no-repeat;} +.jqueryFileTree LI.ext_html{background:url(../img/filetree/html.png) left top no-repeat;} +.jqueryFileTree LI.ext_img{background:url(../img/filetree/disk-image.png) left top no-repeat;} +.jqueryFileTree LI.ext_iso{background:url(../img/filetree/cdrom.png) left top no-repeat;} +.jqueryFileTree LI.ext_jar{background:url(../img/filetree/java.png) left top no-repeat;} +.jqueryFileTree LI.ext_jpg{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_jpeg{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_js{background:url(../img/filetree/script.png) left top no-repeat;} +.jqueryFileTree LI.ext_lasso{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_log{background:url(../img/filetree/txt.png) left top no-repeat;} +.jqueryFileTree LI.ext_m4p{background:url(../img/filetree/music.png) left top no-repeat;} +.jqueryFileTree LI.ext_mov{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_mp3{background:url(../img/filetree/music.png) left top no-repeat;} +.jqueryFileTree LI.ext_mp4{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_mpg{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_mpeg{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_ogg{background:url(../img/filetree/music.png) left top no-repeat;} +.jqueryFileTree LI.ext_pcx{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_pdf{background:url(../img/filetree/pdf.png) left top no-repeat;} +.jqueryFileTree LI.ext_php{background:url(../img/filetree/php.png) left top no-repeat;} +.jqueryFileTree LI.ext_plg{background:url(../img/filetree/plg.png) left top no-repeat;} +.jqueryFileTree LI.ext_png{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_ppt{background:url(../img/filetree/ppt.png) left top no-repeat;} +.jqueryFileTree LI.ext_psd{background:url(../img/filetree/psd.png) left top no-repeat;} +.jqueryFileTree LI.ext_pl{background:url(../img/filetree/script.png) left top no-repeat;} +.jqueryFileTree LI.ext_py{background:url(../img/filetree/script.png) left top no-repeat;} +.jqueryFileTree LI.ext_qcow{background:url(../img/filetree/disk-image.png) left top no-repeat;} +.jqueryFileTree LI.ext_qcow2{background:url(../img/filetree/disk-image.png) left top no-repeat;} +.jqueryFileTree LI.ext_rb{background:url(../img/filetree/ruby.png) left top no-repeat;} +.jqueryFileTree LI.ext_rbx{background:url(../img/filetree/ruby.png) left top no-repeat;} +.jqueryFileTree LI.ext_rhtml{background:url(../img/filetree/ruby.png) left top no-repeat;} +.jqueryFileTree LI.ext_rpm{background:url(../img/filetree/linux.png) left top no-repeat;} +.jqueryFileTree LI.ext_ruby{background:url(../img/filetree/ruby.png) left top no-repeat;} +.jqueryFileTree LI.ext_sql{background:url(../img/filetree/db.png) left top no-repeat;} +.jqueryFileTree LI.ext_swf{background:url(../img/filetree/swf.png) left top no-repeat;} +.jqueryFileTree LI.ext_tif{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_tiff{background:url(../img/filetree/picture.png) left top no-repeat;} +.jqueryFileTree LI.ext_txt{background:url(../img/filetree/txt.png) left top no-repeat;} +.jqueryFileTree LI.ext_vb{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_wav{background:url(../img/filetree/music.png) left top no-repeat;} +.jqueryFileTree LI.ext_wmv{background:url(../img/filetree/film.png) left top no-repeat;} +.jqueryFileTree LI.ext_xls{background:url(../img/filetree/xls.png) left top no-repeat;} +.jqueryFileTree LI.ext_xml{background:url(../img/filetree/code.png) left top no-repeat;} +.jqueryFileTree LI.ext_zip{background:url(../img/filetree/zip.png) left top no-repeat;} diff --git a/web/static/css/nprogress.css b/web/static/css/nprogress.css new file mode 100644 index 0000000..7a288bc --- /dev/null +++ b/web/static/css/nprogress.css @@ -0,0 +1,72 @@ +/* Make clicks pass-through */ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: var(--tblr-primary) !important; + position: fixed; + z-index: 1031; + top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top)); + left: 0; + width: 100%; + height: 2px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0; + width: 100px; + height: 100%; + box-shadow: 0 0 10px var(--tblr-primary), 0 0 5px var(--tblr-primary); + opacity: 1.0; + + -webkit-transform: rotate(0deg) translate(0px, -1px); + -ms-transform: rotate(0deg) translate(0px, -1px); + transform: rotate(0deg) translate(0px, -1px); +} + +/* Remove these to get rid of the spinner */ +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 15px; + right: 15px; +} + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #29d; + border-left-color: #29d; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes nprogress-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + diff --git a/web/static/css/style.css b/web/static/css/style.css new file mode 100644 index 0000000..4a5af12 --- /dev/null +++ b/web/static/css/style.css @@ -0,0 +1,160 @@ +html { + --safe-area-inset-top: 0px; + min-height: calc(100% + env(safe-area-inset-top) + var(--safe-area-inset-top)); +} + +body, .page { + min-height: 100%; +} + +.theme-light .page { + background-image: linear-gradient(180deg, rgba(123, 178, 233, 0.4) 0%, rgba(231, 235, 239, 1) calc(50vh)); +} + +.theme-dark .page { + background-image: linear-gradient(180deg, var(--tblr-body-bg) 0%, var(--tblr-bg-surface) calc(50vh)); +} + +.tooltip-inner { + text-align: left; +} + +.fileTree { + width: 240px; + max-height: 200px; + overflow-y: scroll; + overflow-x: hidden; + position: absolute; + display: none; +} + +.dropzone { + border: 1px dashed var(--tblr-border-color) !important; +} + + +@media (max-width: 992px) { + .navbar ul.navbar-nav { + overflow-y: auto; + max-height: 80vh; + } +} + +.page-wrapper { + padding-top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top) + 51px) !important; + padding-left: env(safe-area-inset-left) !important; + overflow: hidden !important; +} + +.page-wrapper-top-off { + margin-top: calc(0px - env(safe-area-inset-top) - var(--safe-area-inset-top) - 51px) !important; +} + +#navbar-menu { + box-shadow: none!important; + border:0!important; +} + +.offcanvas { + padding-top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top)) !important; + padding-left: env(safe-area-inset-left) !important; +} + +.modal-dialog { + padding-top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top)) !important; +} + + +.fc-toolbar-title { + font-size: 1.5em !important; +} + +.fc-list-event .media_calendar_item_info { + display: block !important; +} + +.fc .fc-list-event:hover td { + background-color: rgba(200, 200, 200, 0.1) !important; +} + +.lit-normal-card { + position:relative; + z-index:1; + --tblr-aspect-ratio:150%; + border:none; +} + +.lit-normal-card:hover { + transform:scale(1.05, 1.05); + opacity:1; +} + +.lit-media-info-background { + background-image: + linear-gradient(180deg, rgba(var(--tblr-body-bg-rgb),0) 50%, rgba(var(--tblr-body-bg-rgb), 1) 100%), + linear-gradient(90deg, rgba(var(--tblr-body-bg-rgb),0) 50%, rgba(var(--tblr-body-bg-rgb), 1) 100%), + linear-gradient(270deg, rgba(var(--tblr-body-bg-rgb),0) 50%, rgba(var(--tblr-body-bg-rgb), 1) 100%); + box-shadow:0 0 0 2px rgb(var(--tblr-body-bg-rgb)); +} + +.theme-light .lit-media-info-background { + background-image: + linear-gradient(180deg, rgba(231, 235, 239, 0) 50%, rgba(231, 235, 239, 1) 100%), + linear-gradient(90deg, rgba(231, 235, 239, 0) 50%, rgba(231, 235, 239, 1) 100%), + linear-gradient(270deg, rgba(231, 235, 239, 0) 50%, rgba(231, 235, 239, 1) 100%); + box-shadow:0 0 0 2px rgb(231, 235, 239); +} + +.lit-media-info-image { + width:233px; + height:350px; +} + +@media (max-width: 767.98px) { + .lit-media-info-image { + width:150px; + height:225px; + } +} + +.lit-person-card { + position:relative; + z-index:1; + --tblr-aspect-ratio:150%; + border:none; + box-shadow:0 0 0 1px #888888 inset,0 .125rem .25rem rgba(0,0,0,0.2); + background-image:linear-gradient(45deg,#99999b,#637599 60%); +} + +.lit-person-card:hover { + transform:scale(1.05, 1.05); + opacity:1; + box-shadow:0 0 0 1px #bbbbbb inset; + background-image:linear-gradient(45deg,#bbbbbd,#8597aa 60%); +} + +.grid-normal-card { + grid-template-columns: repeat(auto-fill,minmax(15rem,1fr)); +} + +.grid-media-card { + grid-template-columns: repeat(auto-fill,minmax(9.375rem,1fr)); +} + +.grid-info-card { + grid-template-columns: repeat(auto-fill,minmax(20rem,1fr)); +} + +.offcanvas-backdrop.show { + opacity: 0.5 !important; + background-color: #000 !important; +} + +.select_logger{ + display:flex; + align-items: center; +} + +.select_logger h5{ + padding-right:5px +} \ No newline at end of file diff --git a/web/static/css/tabler.min.css b/web/static/css/tabler.min.css new file mode 100644 index 0000000..a1eca7c --- /dev/null +++ b/web/static/css/tabler.min.css @@ -0,0 +1,14 @@ +/*! +* Tabler v1.0.0-beta16 (https://tabler.io) +* @version 1.0.0-beta16 +* @link https://tabler.io +* Copyright 2018-2022 The Tabler Authors +* Copyright 2018-2022 codecalm.net Paweł Kuna +* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) +*/ +@charset "UTF-8";:root{--tblr-blue:#206bc4;--tblr-indigo:#4263eb;--tblr-purple:#ae3ec9;--tblr-pink:#d6336c;--tblr-red:#d63939;--tblr-orange:#f76707;--tblr-yellow:#f59f00;--tblr-green:#2fb344;--tblr-teal:#0ca678;--tblr-cyan:#17a2b8;--tblr-black:#000000;--tblr-white:#ffffff;--tblr-gray:#49566c;--tblr-gray-dark:#1d273b;--tblr-gray-100:#f1f5f9;--tblr-gray-200:#e2e8f0;--tblr-gray-300:#c8d3e1;--tblr-gray-400:#9ba9be;--tblr-gray-500:#6c7a91;--tblr-gray-600:#49566c;--tblr-gray-700:#313c52;--tblr-gray-800:#1d273b;--tblr-gray-900:#0f172a;--tblr-primary:#206bc4;--tblr-secondary:#616876;--tblr-success:#2fb344;--tblr-info:#4299e1;--tblr-warning:#f76707;--tblr-danger:#d63939;--tblr-light:#f8fafc;--tblr-dark:#1d273b;--tblr-muted:#616876;--tblr-blue:#206bc4;--tblr-azure:#4299e1;--tblr-indigo:#4263eb;--tblr-purple:#ae3ec9;--tblr-pink:#d6336c;--tblr-red:#d63939;--tblr-orange:#f76707;--tblr-yellow:#f59f00;--tblr-lime:#74b816;--tblr-green:#2fb344;--tblr-teal:#0ca678;--tblr-cyan:#17a2b8;--tblr-facebook:#1877F2;--tblr-twitter:#1da1f2;--tblr-linkedin:#0a66c2;--tblr-google:#dc4e41;--tblr-youtube:#ff0000;--tblr-vimeo:#1ab7ea;--tblr-dribbble:#ea4c89;--tblr-github:#181717;--tblr-instagram:#e4405f;--tblr-pinterest:#bd081c;--tblr-vk:#6383a8;--tblr-rss:#ffa500;--tblr-flickr:#0063dc;--tblr-bitbucket:#0052cc;--tblr-tabler:#206bc4;--tblr-primary-rgb:32,107,196;--tblr-secondary-rgb:97,104,118;--tblr-success-rgb:47,179,68;--tblr-info-rgb:66,153,225;--tblr-warning-rgb:247,103,7;--tblr-danger-rgb:214,57,57;--tblr-light-rgb:248,250,252;--tblr-dark-rgb:29,39,59;--tblr-muted-rgb:97,104,118;--tblr-blue-rgb:32,107,196;--tblr-azure-rgb:66,153,225;--tblr-indigo-rgb:66,99,235;--tblr-purple-rgb:174,62,201;--tblr-pink-rgb:214,51,108;--tblr-red-rgb:214,57,57;--tblr-orange-rgb:247,103,7;--tblr-yellow-rgb:245,159,0;--tblr-lime-rgb:116,184,22;--tblr-green-rgb:47,179,68;--tblr-teal-rgb:12,166,120;--tblr-cyan-rgb:23,162,184;--tblr-facebook-rgb:24,119,242;--tblr-twitter-rgb:29,161,242;--tblr-linkedin-rgb:10,102,194;--tblr-google-rgb:220,78,65;--tblr-youtube-rgb:255,0,0;--tblr-vimeo-rgb:26,183,234;--tblr-dribbble-rgb:234,76,137;--tblr-github-rgb:24,23,23;--tblr-instagram-rgb:228,64,95;--tblr-pinterest-rgb:189,8,28;--tblr-vk-rgb:99,131,168;--tblr-rss-rgb:255,165,0;--tblr-flickr-rgb:0,99,220;--tblr-bitbucket-rgb:0,82,204;--tblr-tabler-rgb:32,107,196;--tblr-white-rgb:255,255,255;--tblr-black-rgb:0,0,0;--tblr-body-color-rgb:29,39,59;--tblr-body-bg-rgb:241,245,249;--tblr-font-sans-serif:-apple-system,BlinkMacSystemFont,San Francisco,Segoe UI,Roboto,Helvetica Neue,sans-serif;--tblr-font-monospace:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;--tblr-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--tblr-body-font-family:var(--tblr-font-sans-serif);--tblr-body-font-size:0.875rem;--tblr-body-font-weight:400;--tblr-body-line-height:1.4285714286;--tblr-body-color:#1d273b;--tblr-body-bg:#f1f5f9;--tblr-border-width:1px;--tblr-border-style:solid;--tblr-border-color:#e6e7e9;--tblr-border-color-translucent:rgba(97, 104, 118, 0.16);--tblr-border-radius:4px;--tblr-border-radius-sm:2px;--tblr-border-radius-lg:8px;--tblr-border-radius-xl:1rem;--tblr-border-radius-2xl:2rem;--tblr-border-radius-pill:100rem;--tblr-link-color:var(--tblr-primary);--tblr-link-hover-color:var(--tblr-primary-darken);--tblr-code-color:var(--tblr-gray-600);--tblr-highlight-bg:#fdeccc}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--tblr-body-font-family);font-size:var(--tblr-body-font-size);font-weight:var(--tblr-body-font-weight);line-height:var(--tblr-body-line-height);color:var(--tblr-body-color);text-align:var(--tblr-body-text-align);background-color:var(--tblr-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}.hr,hr{margin:2rem 0;color:inherit;border:0;border-top:1px solid;opacity:.16}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:var(--tblr-font-weight-medium);line-height:1.2}.h1,h1{font-size:1.5rem}.h2,h2{font-size:1.25rem}.h3,h3{font-size:1rem}.h4,h4{font-size:.875rem}.h5,h5{font-size:.75rem}.h6,h6{font-size:.625rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:85.714285%}.mark,mark{padding:.1875em;background-color:var(--tblr-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--tblr-link-color);text-decoration:none}a:hover{color:var(--tblr-link-hover-color);text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--tblr-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:85.714285%;color:var(--tblr-light)}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:85.714285%;color:var(--tblr-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:2px 4px;font-size:85.714285%;color:var(--tblr-muted);background-color:var(--tblr-code-bg);border-radius:2px}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#616876;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:.875rem;font-weight:var(--tblr-font-weight-normal)}.display-1{font-size:5rem;font-weight:300;line-height:1.2}.display-2{font-size:4.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}.display-5{font-size:3rem;font-weight:300;line-height:1.2}.display-6{font-size:2rem;font-weight:300;line-height:1.2}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:85.714285%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:.875rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:85.714285%;color:#49566c}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#f1f5f9;border:1px solid var(--tblr-border-color);border-radius:4px;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:85.714285%;color:#49566c}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--tblr-gutter-x:1.5rem;--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--tblr-gutter-x:1rem;--tblr-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--tblr-gutter-y));margin-right:calc(-.5 * var(--tblr-gutter-x));margin-left:calc(-.5 * var(--tblr-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-top:var(--tblr-gutter-y)}.grid{display:grid;grid-template-rows:repeat(var(--tblr-rows,1),1fr);grid-template-columns:repeat(var(--tblr-columns,12),1fr);gap:var(--tblr-gap,1rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media (min-width:576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media (min-width:768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media (min-width:992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media (min-width:1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media (min-width:1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--tblr-gutter-x:0}.g-0,.gy-0{--tblr-gutter-y:0}.g-1,.gx-1{--tblr-gutter-x:0.25rem}.g-1,.gy-1{--tblr-gutter-y:0.25rem}.g-2,.gx-2{--tblr-gutter-x:0.5rem}.g-2,.gy-2{--tblr-gutter-y:0.5rem}.g-3,.gx-3{--tblr-gutter-x:1rem}.g-3,.gy-3{--tblr-gutter-y:1rem}.g-4,.gx-4{--tblr-gutter-x:2rem}.g-4,.gy-4{--tblr-gutter-y:2rem}.g-5,.gx-5{--tblr-gutter-x:4rem}.g-5,.gy-5{--tblr-gutter-y:4rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--tblr-gutter-x:0}.g-sm-0,.gy-sm-0{--tblr-gutter-y:0}.g-sm-1,.gx-sm-1{--tblr-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--tblr-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--tblr-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--tblr-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--tblr-gutter-x:1rem}.g-sm-3,.gy-sm-3{--tblr-gutter-y:1rem}.g-sm-4,.gx-sm-4{--tblr-gutter-x:2rem}.g-sm-4,.gy-sm-4{--tblr-gutter-y:2rem}.g-sm-5,.gx-sm-5{--tblr-gutter-x:4rem}.g-sm-5,.gy-sm-5{--tblr-gutter-y:4rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--tblr-gutter-x:0}.g-md-0,.gy-md-0{--tblr-gutter-y:0}.g-md-1,.gx-md-1{--tblr-gutter-x:0.25rem}.g-md-1,.gy-md-1{--tblr-gutter-y:0.25rem}.g-md-2,.gx-md-2{--tblr-gutter-x:0.5rem}.g-md-2,.gy-md-2{--tblr-gutter-y:0.5rem}.g-md-3,.gx-md-3{--tblr-gutter-x:1rem}.g-md-3,.gy-md-3{--tblr-gutter-y:1rem}.g-md-4,.gx-md-4{--tblr-gutter-x:2rem}.g-md-4,.gy-md-4{--tblr-gutter-y:2rem}.g-md-5,.gx-md-5{--tblr-gutter-x:4rem}.g-md-5,.gy-md-5{--tblr-gutter-y:4rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--tblr-gutter-x:0}.g-lg-0,.gy-lg-0{--tblr-gutter-y:0}.g-lg-1,.gx-lg-1{--tblr-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--tblr-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--tblr-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--tblr-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--tblr-gutter-x:1rem}.g-lg-3,.gy-lg-3{--tblr-gutter-y:1rem}.g-lg-4,.gx-lg-4{--tblr-gutter-x:2rem}.g-lg-4,.gy-lg-4{--tblr-gutter-y:2rem}.g-lg-5,.gx-lg-5{--tblr-gutter-x:4rem}.g-lg-5,.gy-lg-5{--tblr-gutter-y:4rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--tblr-gutter-x:0}.g-xl-0,.gy-xl-0{--tblr-gutter-y:0}.g-xl-1,.gx-xl-1{--tblr-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--tblr-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--tblr-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--tblr-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--tblr-gutter-x:1rem}.g-xl-3,.gy-xl-3{--tblr-gutter-y:1rem}.g-xl-4,.gx-xl-4{--tblr-gutter-x:2rem}.g-xl-4,.gy-xl-4{--tblr-gutter-y:2rem}.g-xl-5,.gx-xl-5{--tblr-gutter-x:4rem}.g-xl-5,.gy-xl-5{--tblr-gutter-y:4rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--tblr-gutter-x:0}.g-xxl-0,.gy-xxl-0{--tblr-gutter-y:0}.g-xxl-1,.gx-xxl-1{--tblr-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--tblr-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--tblr-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--tblr-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--tblr-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--tblr-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--tblr-gutter-x:2rem}.g-xxl-4,.gy-xxl-4{--tblr-gutter-y:2rem}.g-xxl-5,.gx-xxl-5{--tblr-gutter-x:4rem}.g-xxl-5,.gy-xxl-5{--tblr-gutter-y:4rem}}.markdown>table,.table{--tblr-table-color:inherit;--tblr-table-bg:transparent;--tblr-table-border-color:var(--tblr-border-color-translucent);--tblr-table-accent-bg:transparent;--tblr-table-striped-color:inherit;--tblr-table-striped-bg:var(--tblr-bg-surface-secondary);--tblr-table-active-color:inherit;--tblr-table-active-bg:rgba(0, 0, 0, 0.1);--tblr-table-hover-color:inherit;--tblr-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--tblr-table-color);vertical-align:top;border-color:var(--tblr-table-border-color)}.markdown>table>:not(caption)>*>*,.table>:not(caption)>*>*{padding:.75rem .75rem;background-color:var(--tblr-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--tblr-table-accent-bg)}.markdown>table>tbody,.table>tbody{vertical-align:inherit}.markdown>table>thead,.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid var(--tblr-border-color-translucent)}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.markdown>table>:not(caption)>*,.table-bordered>:not(caption)>*{border-width:1px 0}.markdown>table>:not(caption)>*>*,.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(even)>*{--tblr-table-accent-bg:var(--tblr-table-striped-bg);color:var(--tblr-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--tblr-table-accent-bg:var(--tblr-table-striped-bg);color:var(--tblr-table-striped-color)}.table-active{--tblr-table-accent-bg:var(--tblr-table-active-bg);color:var(--tblr-table-active-color)}.table-hover>tbody>tr:hover>*{--tblr-table-accent-bg:var(--tblr-table-hover-bg);color:var(--tblr-table-hover-color)}.table-primary{--tblr-table-color:#1d273b;--tblr-table-bg:#d2e1f3;--tblr-table-border-color:#c0cee1;--tblr-table-striped-bg:#c9d8ea;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#c0cee1;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#c4d3e5;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-secondary{--tblr-table-color:#1d273b;--tblr-table-bg:#dfe1e4;--tblr-table-border-color:#ccced3;--tblr-table-striped-bg:#d5d8dc;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#ccced3;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#d0d3d7;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-success{--tblr-table-color:#1d273b;--tblr-table-bg:#d5f0da;--tblr-table-border-color:#c3dcca;--tblr-table-striped-bg:#cce6d2;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#c3dcca;--tblr-table-active-color:#1d273b;--tblr-table-hover-bg:#c7e1ce;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-info{--tblr-table-color:#1d273b;--tblr-table-bg:#d9ebf9;--tblr-table-border-color:#c6d7e6;--tblr-table-striped-bg:#d0e1f0;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#c6d7e6;--tblr-table-active-color:#1d273b;--tblr-table-hover-bg:#cbdceb;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-warning{--tblr-table-color:#1d273b;--tblr-table-bg:#fde1cd;--tblr-table-border-color:#e7cebe;--tblr-table-striped-bg:#f2d8c6;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#e7cebe;--tblr-table-active-color:#1d273b;--tblr-table-hover-bg:#ecd3c2;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-danger{--tblr-table-color:#1d273b;--tblr-table-bg:#f7d7d7;--tblr-table-border-color:#e1c5c7;--tblr-table-striped-bg:#eccecf;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#e1c5c7;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#e7cacb;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-light{--tblr-table-color:#1d273b;--tblr-table-bg:#f8fafc;--tblr-table-border-color:#e2e5e9;--tblr-table-striped-bg:#edeff2;--tblr-table-striped-color:#1d273b;--tblr-table-active-bg:#e2e5e9;--tblr-table-active-color:#1d273b;--tblr-table-hover-bg:#e8eaee;--tblr-table-hover-color:#1d273b;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-dark{--tblr-table-color:#f8fafc;--tblr-table-bg:#1d273b;--tblr-table-border-color:#333c4e;--tblr-table-striped-bg:#283245;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#333c4e;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#2d3749;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem;font-size:.875rem;font-weight:var(--tblr-font-weight-medium)}.col-form-label{padding-top:calc(.4375rem + 1px);padding-bottom:calc(.4375rem + 1px);margin-bottom:0;font-size:inherit;font-weight:var(--tblr-font-weight-medium);line-height:1.4285714286}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.125rem + 1px);padding-bottom:calc(.125rem + 1px);font-size:.75rem}.form-text{margin-top:.25rem;font-size:85.714285%;color:#616876}.form-control{display:block;width:100%;padding:.4375rem .75rem;font-family:var(--tblr-font-sans-serif);font-size:.875rem;font-weight:400;line-height:1.4285714286;color:inherit;background-color:var(--tblr-bg-forms);background-clip:padding-box;border:1px solid var(--tblr-border-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--tblr-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:inherit;background-color:var(--tblr-bg-forms);border-color:#90b5e2;outline:0;box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-control::-webkit-date-and-time-value{height:1.4285714286em}.form-control::-webkit-input-placeholder{color:#a5a9b1;opacity:1}.form-control::-moz-placeholder{color:#a5a9b1;opacity:1}.form-control:-ms-input-placeholder{color:#a5a9b1;opacity:1}.form-control::-ms-input-placeholder{color:#a5a9b1;opacity:1}.form-control::placeholder{color:#a5a9b1;opacity:1}.form-control:disabled{background-color:var(--tblr-gray-100);opacity:1}.form-control::-webkit-file-upload-button{padding:.4375rem .75rem;margin:-.4375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:inherit;background-color:#f8fafc;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.4375rem .75rem;margin:-.4375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:inherit;background-color:#f8fafc;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#eceeef}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#eceeef}.form-control-plaintext{display:block;width:100%;padding:.4375rem 0;margin-bottom:0;line-height:1.4285714286;color:#1d273b;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.4285714286em + .25rem + 2px);padding:.125rem .25rem;font-size:.75rem;border-radius:2px}.form-control-sm::-webkit-file-upload-button{padding:.125rem .25rem;margin:-.125rem -.25rem;-webkit-margin-end:.25rem;margin-inline-end:.25rem}.form-control-sm::file-selector-button{padding:.125rem .25rem;margin:-.125rem -.25rem;-webkit-margin-end:.25rem;margin-inline-end:.25rem}.form-control-lg{min-height:calc(1.4285714286em + 1rem + 2px);padding:.5rem .75rem;font-size:1.25rem;border-radius:8px}.form-control-lg::-webkit-file-upload-button{padding:.5rem .75rem;margin:-.5rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem}.form-control-lg::file-selector-button{padding:.5rem .75rem;margin:-.5rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem}textarea.form-control{min-height:calc(1.4285714286em + .875rem + 2px)}textarea.form-control-sm{min-height:calc(1.4285714286em + .25rem + 2px)}textarea.form-control-lg{min-height:calc(1.4285714286em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.4285714286em + .875rem + 2px);padding:.4375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--tblr-border-radius)}.form-control-color::-webkit-color-swatch{border-radius:var(--tblr-border-radius)}.form-control-color.form-control-sm{height:calc(1.4285714286em + .25rem + 2px)}.form-control-color.form-control-lg{height:calc(1.4285714286em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.4375rem 2.25rem .4375rem .75rem;-moz-padding-start:calc(.75rem - 3px);font-family:var(--tblr-font-sans-serif);font-size:.875rem;font-weight:400;line-height:1.4285714286;color:inherit;background-color:var(--tblr-bg-forms);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a5a9b1' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid var(--tblr-border-color);border-radius:var(--tblr-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#90b5e2;outline:0;box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e2e8f0}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 inherit}.form-select-sm{padding-top:.125rem;padding-bottom:.125rem;padding-left:.25rem;font-size:.75rem;border-radius:2px}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:.75rem;font-size:1.25rem;border-radius:8px}.form-check{display:block;min-height:1.25rem;padding-left:1.5rem;margin-bottom:.5rem}.form-check .form-check-input{float:left;margin-left:-1.5rem}.form-check-reverse{padding-right:1.5rem;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5rem;margin-left:0}.form-check-input{width:1rem;height:1rem;margin-top:.2142857143rem;vertical-align:top;background-color:var(--tblr-bg-forms);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:var(--tblr-border-radius)}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#90b5e2;outline:0;box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-check-input:checked{background-color:var(--tblr-primary);border-color:var(--tblr-border-color-translucent)}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#206bc4;border-color:#206bc4;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.7}.form-switch{padding-left:2.5rem}.form-switch .form-check-input{width:2rem;margin-left:-2.5rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23e6e7e9'/%3e%3c/svg%3e");background-position:left center;border-radius:2rem;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2390b5e2'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5rem;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5rem;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.4}.form-range{width:100%;height:1.25rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f1f5f9,0 0 0 .25rem rgba(32,107,196,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f1f5f9,0 0 0 .25rem rgba(32,107,196,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.375rem;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #fff;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bcd3ed}.form-range::-webkit-slider-runnable-track{width:100%;height:.25rem;color:transparent;cursor:pointer;background-color:var(--tblr-border-color);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #fff;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#bcd3ed}.form-range::-moz-range-track{width:100%;height:.25rem;color:transparent;cursor:pointer;background-color:var(--tblr-border-color);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#6c7a91}.form-range:disabled::-moz-range-thumb{background-color:#6c7a91}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-webkit-input-placeholder,.form-floating>.form-control::-webkit-input-placeholder{color:transparent}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext:-ms-input-placeholder,.form-floating>.form-control:-ms-input-placeholder{color:transparent}.form-floating>.form-control-plaintext::-ms-input-placeholder,.form-floating>.form-control::-ms-input-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:not(:-ms-input-placeholder),.form-floating>.form-control:not(:-ms-input-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-ms-input-placeholder)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.4375rem .75rem;font-size:.875rem;font-weight:400;line-height:1.4285714286;color:var(--tblr-muted);text-align:center;white-space:nowrap;background-color:#f8fafc;border:1px solid var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem .75rem;font-size:1.25rem;border-radius:8px}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.125rem .25rem;font-size:.75rem;border-radius:2px}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:85.714285%;color:#2fb344}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.765625rem;color:#f8fafc;background-color:rgba(47,179,68,.9);border-radius:4px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#2fb344;padding-right:calc(1.4285714286em + .875rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.3571428572em + .21875rem) center;background-size:calc(.7142857143em + .4375rem) calc(.7142857143em + .4375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#2fb344;box-shadow:0 0 0 .25rem rgba(47,179,68,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.4285714286em + .875rem);background-position:top calc(.3571428572em + .21875rem) right calc(.3571428572em + .21875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#2fb344}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a5a9b1' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.7142857143em + .4375rem) calc(.7142857143em + .4375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#2fb344;box-shadow:0 0 0 .25rem rgba(47,179,68,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.4285714286em + .875rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#2fb344}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#2fb344}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(47,179,68,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#2fb344}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:85.714285%;color:#d63939}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.765625rem;color:#f8fafc;background-color:rgba(214,57,57,.9);border-radius:4px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#d63939;padding-right:calc(1.4285714286em + .875rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.3571428572em + .21875rem) center;background-size:calc(.7142857143em + .4375rem) calc(.7142857143em + .4375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#d63939;box-shadow:0 0 0 .25rem rgba(214,57,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.4285714286em + .875rem);background-position:top calc(.3571428572em + .21875rem) right calc(.3571428572em + .21875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#d63939}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a5a9b1' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.7142857143em + .4375rem) calc(.7142857143em + .4375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#d63939;box-shadow:0 0 0 .25rem rgba(214,57,57,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.4285714286em + .875rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#d63939}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#d63939}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(214,57,57,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#d63939}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--tblr-btn-padding-x:1rem;--tblr-btn-padding-y:0.4375rem;--tblr-btn-font-family:var(--tblr-font-sans-serif);--tblr-btn-font-size:0.875rem;--tblr-btn-font-weight:var(--tblr-font-weight-medium);--tblr-btn-line-height:1.4285714286;--tblr-btn-color:#1d273b;--tblr-btn-bg:transparent;--tblr-btn-border-width:1px;--tblr-btn-border-color:transparent;--tblr-btn-border-radius:var(--tblr-border-radius);--tblr-btn-hover-border-color:transparent;--tblr-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--tblr-btn-disabled-opacity:0.4;--tblr-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--tblr-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--tblr-btn-padding-y) var(--tblr-btn-padding-x);font-family:var(--tblr-btn-font-family);font-size:var(--tblr-btn-font-size);font-weight:var(--tblr-btn-font-weight);line-height:var(--tblr-btn-line-height);color:var(--tblr-btn-color);text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:var(--tblr-btn-border-width) solid var(--tblr-btn-border-color);border-radius:var(--tblr-btn-border-radius);background-color:var(--tblr-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--tblr-btn-hover-color);text-decoration:none;background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--tblr-btn-color);background-color:var(--tblr-btn-bg);border-color:var(--tblr-btn-border-color)}.btn:focus-visible{color:var(--tblr-btn-hover-color);background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color);outline:0;box-shadow:var(--tblr-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--tblr-btn-hover-border-color);outline:0;box-shadow:var(--tblr-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--tblr-btn-active-color);background-color:var(--tblr-btn-active-bg);border-color:var(--tblr-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--tblr-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--tblr-btn-disabled-color);pointer-events:none;background-color:var(--tblr-btn-disabled-bg);border-color:var(--tblr-btn-disabled-border-color);opacity:var(--tblr-btn-disabled-opacity)}.btn-link{--tblr-btn-font-weight:400;--tblr-btn-color:var(--tblr-link-color);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-link-hover-color);--tblr-btn-hover-border-color:transparent;--tblr-btn-active-color:var(--tblr-link-hover-color);--tblr-btn-active-border-color:transparent;--tblr-btn-disabled-color:#49566c;--tblr-btn-disabled-border-color:transparent;--tblr-btn-box-shadow:none;--tblr-btn-focus-shadow-rgb:64,128,204;text-decoration:none}.btn-link:focus-visible,.btn-link:hover{text-decoration:underline}.btn-link:focus-visible{color:var(--tblr-btn-color)}.btn-link:hover{color:var(--tblr-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--tblr-btn-padding-y:0.5rem;--tblr-btn-padding-x:0.75rem;--tblr-btn-font-size:1.25rem;--tblr-btn-border-radius:8px}.btn-group-sm>.btn,.btn-sm{--tblr-btn-padding-y:0.125rem;--tblr-btn-padding-x:0.25rem;--tblr-btn-font-size:0.75rem;--tblr-btn-border-radius:2px}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-45deg)}.dropdown-menu{--tblr-dropdown-zindex:1000;--tblr-dropdown-min-width:11rem;--tblr-dropdown-padding-x:0;--tblr-dropdown-padding-y:0.25rem;--tblr-dropdown-spacer:1px;--tblr-dropdown-font-size:0.875rem;--tblr-dropdown-color:#1d273b;--tblr-dropdown-bg:#ffffff;--tblr-dropdown-border-color:var(--tblr-border-color-translucent);--tblr-dropdown-border-radius:4px;--tblr-dropdown-border-width:1px;--tblr-dropdown-inner-border-radius:3px;--tblr-dropdown-divider-bg:var(--tblr-border-color-translucent);--tblr-dropdown-divider-margin-y:0.5rem;--tblr-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--tblr-dropdown-link-color:inherit;--tblr-dropdown-link-hover-color:inherit;--tblr-dropdown-link-hover-bg:rgba(var(--tblr-muted-rgb), 0.04);--tblr-dropdown-link-active-color:var(--tblr-primary);--tblr-dropdown-link-active-bg:var(--tblr-active-bg);--tblr-dropdown-link-disabled-color:#6c7a91;--tblr-dropdown-item-padding-x:0.75rem;--tblr-dropdown-item-padding-y:0.5rem;--tblr-dropdown-header-color:#49566c;--tblr-dropdown-header-padding-x:0.75rem;--tblr-dropdown-header-padding-y:0.25rem;position:absolute;z-index:var(--tblr-dropdown-zindex);display:none;min-width:var(--tblr-dropdown-min-width);padding:var(--tblr-dropdown-padding-y) var(--tblr-dropdown-padding-x);margin:0;font-size:var(--tblr-dropdown-font-size);color:var(--tblr-dropdown-color);text-align:left;list-style:none;background-color:var(--tblr-dropdown-bg);background-clip:padding-box;border:var(--tblr-dropdown-border-width) solid var(--tblr-dropdown-border-color);border-radius:var(--tblr-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--tblr-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--tblr-dropdown-spacer)}.dropup .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(135deg)}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--tblr-dropdown-spacer)}.dropend .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-135deg)}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--tblr-dropdown-spacer)}.dropstart .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(45deg)}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--tblr-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--tblr-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--tblr-dropdown-item-padding-y) var(--tblr-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--tblr-dropdown-link-color);text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--tblr-dropdown-link-hover-color);text-decoration:none;background-color:var(--tblr-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--tblr-dropdown-link-active-color);text-decoration:none;background-color:var(--tblr-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--tblr-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--tblr-dropdown-header-padding-y) var(--tblr-dropdown-header-padding-x);margin-bottom:0;font-size:.765625rem;color:var(--tblr-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--tblr-dropdown-item-padding-y) var(--tblr-dropdown-item-padding-x);color:var(--tblr-dropdown-link-color)}.dropdown-menu-dark{--tblr-dropdown-color:#c8d3e1;--tblr-dropdown-bg:#1d273b;--tblr-dropdown-border-color:var(--tblr-border-color-translucent);--tblr-dropdown-link-color:#c8d3e1;--tblr-dropdown-link-hover-color:#ffffff;--tblr-dropdown-divider-bg:var(--tblr-border-color-translucent);--tblr-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--tblr-dropdown-link-active-color:var(--tblr-primary);--tblr-dropdown-link-active-bg:var(--tblr-active-bg);--tblr-dropdown-link-disabled-color:#6c7a91;--tblr-dropdown-header-color:#6c7a91}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--tblr-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.1875rem;padding-left:.1875rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--tblr-nav-link-padding-x:0.75rem;--tblr-nav-link-padding-y:0.5rem;--tblr-nav-link-color:var(--tblr-muted);--tblr-nav-link-hover-color:var(--tblr-link-hover-color);--tblr-nav-link-disabled-color:var(--tblr-disabled-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--tblr-nav-link-padding-y) var(--tblr-nav-link-padding-x);font-size:var(--tblr-nav-link-font-size);font-weight:var(--tblr-nav-link-font-weight);color:var(--tblr-nav-link-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--tblr-nav-link-hover-color);text-decoration:none}.nav-link.disabled{color:var(--tblr-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--tblr-nav-tabs-border-width:1px;--tblr-nav-tabs-border-color:var(--tblr-border-color);--tblr-nav-tabs-border-radius:var(--tblr-border-radius);--tblr-nav-tabs-link-hover-border-color:var(--tblr-border-color) var(--tblr-border-color) var(--tblr-border-color);--tblr-nav-tabs-link-active-color:var(--tblr-body-color);--tblr-nav-tabs-link-active-bg:#f1f5f9;--tblr-nav-tabs-link-active-border-color:var(--tblr-border-color) var(--tblr-border-color) var(--tblr-border-color);border-bottom:var(--tblr-nav-tabs-border-width) solid var(--tblr-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--tblr-nav-tabs-border-width));background:0 0;border:var(--tblr-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--tblr-nav-tabs-border-radius);border-top-right-radius:var(--tblr-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--tblr-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--tblr-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--tblr-nav-tabs-link-active-color);background-color:var(--tblr-nav-tabs-link-active-bg);border-color:var(--tblr-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--tblr-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--tblr-nav-pills-border-radius:4px;--tblr-nav-pills-link-active-color:var(--tblr-primary);--tblr-nav-pills-link-active-bg:var(--tblr-active-bg)}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--tblr-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--tblr-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--tblr-nav-pills-link-active-color);background-color:var(--tblr-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--tblr-navbar-padding-x:0;--tblr-navbar-padding-y:0.25rem;--tblr-navbar-color:var(--tblr-body-color);--tblr-navbar-hover-color:rgba(0, 0, 0, 0.7);--tblr-navbar-disabled-color:var(--tblr-disabled-color);--tblr-navbar-active-color:var(--tblr-body-color) color;--tblr-navbar-brand-padding-y:0.5rem;--tblr-navbar-brand-margin-end:1rem;--tblr-navbar-brand-font-size:1rem;--tblr-navbar-brand-color:var(--tblr-body-color);--tblr-navbar-brand-hover-color:var(--tblr-body-color) color;--tblr-navbar-nav-link-padding-x:0.75rem;--tblr-navbar-toggler-padding-y:0;--tblr-navbar-toggler-padding-x:0;--tblr-navbar-toggler-font-size:1rem;--tblr-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='var%28--tblr-body-color%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--tblr-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--tblr-navbar-toggler-border-radius:var(--tblr-border-radius);--tblr-navbar-toggler-focus-width:0;--tblr-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--tblr-navbar-padding-y) var(--tblr-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--tblr-navbar-brand-padding-y);padding-bottom:var(--tblr-navbar-brand-padding-y);margin-right:var(--tblr-navbar-brand-margin-end);font-size:var(--tblr-navbar-brand-font-size);color:var(--tblr-navbar-brand-color);white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--tblr-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--tblr-nav-link-padding-x:0;--tblr-nav-link-padding-y:0.5rem;--tblr-nav-link-color:var(--tblr-navbar-color);--tblr-nav-link-hover-color:var(--tblr-navbar-hover-color);--tblr-nav-link-disabled-color:var(--tblr-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--tblr-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--tblr-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--tblr-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--tblr-navbar-toggler-padding-y) var(--tblr-navbar-toggler-padding-x);font-size:var(--tblr-navbar-toggler-font-size);line-height:1;color:var(--tblr-navbar-color);background-color:transparent;border:var(--tblr-border-width) solid var(--tblr-navbar-toggler-border-color);border-radius:var(--tblr-navbar-toggler-border-radius);transition:var(--tblr-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--tblr-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--tblr-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--tblr-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--tblr-navbar-color:rgba(255, 255, 255, 0.7);--tblr-navbar-hover-color:rgba(255, 255, 255, 0.75);--tblr-navbar-disabled-color:var(--tblr-disabled-color);--tblr-navbar-active-color:#ffffff;--tblr-navbar-brand-color:#ffffff;--tblr-navbar-brand-hover-color:#ffffff;--tblr-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--tblr-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--tblr-card-spacer-y:1rem;--tblr-card-spacer-x:1.5rem;--tblr-card-title-spacer-y:1.25rem;--tblr-card-border-width:var(--tblr-border-width);--tblr-card-border-color:var(--tblr-border-color);--tblr-card-border-radius:var(--tblr-border-radius);--tblr-card-box-shadow:var(--tblr-shadow-card);--tblr-card-inner-border-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)));--tblr-card-cap-padding-y:1rem;--tblr-card-cap-padding-x:1.5rem;--tblr-card-cap-bg:var(--tblr-bg-surface-secondary);--tblr-card-cap-color:inherit;--tblr-card-color:inherit;--tblr-card-bg:var(--tblr-bg-surface);--tblr-card-img-overlay-padding:1rem;--tblr-card-group-margin:1.5rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--tblr-card-height);word-wrap:break-word;background-color:var(--tblr-card-bg);background-clip:border-box;border:var(--tblr-card-border-width) solid var(--tblr-card-border-color);border-radius:var(--tblr-card-border-radius)}.card>.hr,.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--tblr-card-inner-border-radius);border-top-right-radius:var(--tblr-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--tblr-card-inner-border-radius);border-bottom-left-radius:var(--tblr-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--tblr-card-spacer-y) var(--tblr-card-spacer-x);color:var(--tblr-card-color)}.card-title{margin-bottom:var(--tblr-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--tblr-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:var(--tblr-card-spacer-x)}.card-header{padding:var(--tblr-card-cap-padding-y) var(--tblr-card-cap-padding-x);margin-bottom:0;color:var(--tblr-card-cap-color);background-color:var(--tblr-card-cap-bg);border-bottom:var(--tblr-card-border-width) solid var(--tblr-card-border-color)}.card-header:first-child{border-radius:var(--tblr-card-inner-border-radius) var(--tblr-card-inner-border-radius) 0 0}.card-footer{padding:var(--tblr-card-cap-padding-y) var(--tblr-card-cap-padding-x);color:var(--tblr-card-cap-color);background-color:var(--tblr-card-cap-bg);border-top:var(--tblr-card-border-width) solid var(--tblr-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--tblr-card-inner-border-radius) var(--tblr-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--tblr-card-cap-padding-x));margin-bottom:calc(-1 * var(--tblr-card-cap-padding-y));margin-left:calc(-.5 * var(--tblr-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--tblr-card-bg);border-bottom-color:var(--tblr-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--tblr-card-cap-padding-x));margin-left:calc(-.5 * var(--tblr-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--tblr-card-img-overlay-padding);border-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--tblr-card-inner-border-radius);border-top-right-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--tblr-card-inner-border-radius);border-bottom-left-radius:var(--tblr-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--tblr-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--tblr-accordion-color:var(--tblr-body-color);--tblr-accordion-bg:transparent;--tblr-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--tblr-accordion-border-color:var(--tblr-border-color-translucent);--tblr-accordion-border-width:1px;--tblr-accordion-border-radius:4px;--tblr-accordion-inner-border-radius:3px;--tblr-accordion-btn-padding-x:1.25rem;--tblr-accordion-btn-padding-y:1rem;--tblr-accordion-btn-color:var(--tblr-body-color);--tblr-accordion-btn-bg:transparent;--tblr-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--tblr-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--tblr-accordion-btn-icon-width:1rem;--tblr-accordion-btn-icon-transform:rotate(-180deg);--tblr-accordion-btn-icon-transition:transform 0.2s ease-in-out;--tblr-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='inherit'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--tblr-accordion-btn-focus-border-color:var(--tblr-border-color-translucent);--tblr-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(32, 107, 196, 0.25);--tblr-accordion-body-padding-x:1.25rem;--tblr-accordion-body-padding-y:1rem;--tblr-accordion-active-color:inherit;--tblr-accordion-active-bg:transparent}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--tblr-accordion-btn-padding-y) var(--tblr-accordion-btn-padding-x);font-size:.875rem;color:var(--tblr-accordion-btn-color);text-align:left;background-color:var(--tblr-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--tblr-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--tblr-accordion-active-color);background-color:var(--tblr-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--tblr-accordion-border-width)) 0 var(--tblr-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--tblr-accordion-btn-active-icon);transform:var(--tblr-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--tblr-accordion-btn-icon-width);height:var(--tblr-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--tblr-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--tblr-accordion-btn-icon-width);transition:var(--tblr-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--tblr-accordion-btn-focus-border-color);outline:0;box-shadow:var(--tblr-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--tblr-accordion-color);background-color:var(--tblr-accordion-bg);border:var(--tblr-accordion-border-width) solid var(--tblr-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--tblr-accordion-border-radius);border-top-right-radius:var(--tblr-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--tblr-accordion-inner-border-radius);border-top-right-radius:var(--tblr-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--tblr-accordion-border-radius);border-bottom-left-radius:var(--tblr-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--tblr-accordion-inner-border-radius);border-bottom-left-radius:var(--tblr-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--tblr-accordion-border-radius);border-bottom-left-radius:var(--tblr-accordion-border-radius)}.accordion-body{padding:var(--tblr-accordion-body-padding-y) var(--tblr-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}.breadcrumb{--tblr-breadcrumb-padding-x:0;--tblr-breadcrumb-padding-y:0;--tblr-breadcrumb-margin-bottom:1rem;--tblr-breadcrumb-divider-color:var(--tblr-muted);--tblr-breadcrumb-item-padding-x:0.5rem;--tblr-breadcrumb-item-active-color:inherit;display:flex;flex-wrap:wrap;padding:var(--tblr-breadcrumb-padding-y) var(--tblr-breadcrumb-padding-x);margin-bottom:var(--tblr-breadcrumb-margin-bottom);font-size:var(--tblr-breadcrumb-font-size);list-style:none;background-color:var(--tblr-breadcrumb-bg);border-radius:var(--tblr-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--tblr-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--tblr-breadcrumb-item-padding-x);color:var(--tblr-breadcrumb-divider-color);content:var(--tblr-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--tblr-breadcrumb-item-active-color)}.pagination{--tblr-pagination-padding-x:0.25rem;--tblr-pagination-padding-y:0.25rem;--tblr-pagination-font-size:0.875rem;--tblr-pagination-color:var(--tblr-muted);--tblr-pagination-bg:transparent;--tblr-pagination-border-width:0;--tblr-pagination-border-color:#c8d3e1;--tblr-pagination-border-radius:4px;--tblr-pagination-hover-color:var(--tblr-link-hover-color);--tblr-pagination-hover-bg:#e2e8f0;--tblr-pagination-hover-border-color:#c8d3e1;--tblr-pagination-focus-color:var(--tblr-link-hover-color);--tblr-pagination-focus-bg:#e2e8f0;--tblr-pagination-focus-box-shadow:0 0 0 0.25rem rgba(32, 107, 196, 0.25);--tblr-pagination-active-color:#ffffff;--tblr-pagination-active-bg:var(--tblr-primary);--tblr-pagination-active-border-color:var(--tblr-primary);--tblr-pagination-disabled-color:var(--tblr-disabled-color);--tblr-pagination-disabled-bg:transparent;--tblr-pagination-disabled-border-color:#c8d3e1;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--tblr-pagination-padding-y) var(--tblr-pagination-padding-x);font-size:var(--tblr-pagination-font-size);color:var(--tblr-pagination-color);background-color:var(--tblr-pagination-bg);border:var(--tblr-pagination-border-width) solid var(--tblr-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--tblr-pagination-hover-color);text-decoration:none;background-color:var(--tblr-pagination-hover-bg);border-color:var(--tblr-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--tblr-pagination-focus-color);background-color:var(--tblr-pagination-focus-bg);outline:0;box-shadow:var(--tblr-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--tblr-pagination-active-color);background-color:var(--tblr-pagination-active-bg);border-color:var(--tblr-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--tblr-pagination-disabled-color);pointer-events:none;background-color:var(--tblr-pagination-disabled-bg);border-color:var(--tblr-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:0}.page-item:first-child .page-link{border-top-left-radius:var(--tblr-pagination-border-radius);border-bottom-left-radius:var(--tblr-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--tblr-pagination-border-radius);border-bottom-right-radius:var(--tblr-pagination-border-radius)}.pagination-lg{--tblr-pagination-padding-x:1.5rem;--tblr-pagination-padding-y:0.75rem;--tblr-pagination-font-size:1.09375rem;--tblr-pagination-border-radius:8px}.pagination-sm{--tblr-pagination-padding-x:0.5rem;--tblr-pagination-padding-y:0.25rem;--tblr-pagination-font-size:0.765625rem;--tblr-pagination-border-radius:2px}.badge{--tblr-badge-padding-x:0.5em;--tblr-badge-padding-y:0.25em;--tblr-badge-font-size:85.714285%;--tblr-badge-font-weight:var(--tblr-font-weight-medium);--tblr-badge-color:#ffffff;--tblr-badge-border-radius:4px;display:inline-block;padding:var(--tblr-badge-padding-y) var(--tblr-badge-padding-x);font-size:var(--tblr-badge-font-size);font-weight:var(--tblr-badge-font-weight);line-height:1;color:var(--tblr-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--tblr-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--tblr-alert-bg:transparent;--tblr-alert-padding-x:1rem;--tblr-alert-padding-y:1rem;--tblr-alert-margin-bottom:1rem;--tblr-alert-color:inherit;--tblr-alert-border-color:transparent;--tblr-alert-border:1px solid var(--tblr-alert-border-color);--tblr-alert-border-radius:4px;position:relative;padding:var(--tblr-alert-padding-y) var(--tblr-alert-padding-x);margin-bottom:var(--tblr-alert-margin-bottom);color:var(--tblr-alert-color);background-color:var(--tblr-alert-bg);border:var(--tblr-alert-border);border-radius:var(--tblr-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:var(--tblr-font-weight-bold)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress{--tblr-progress-height:0.5rem;--tblr-progress-font-size:0.65625rem;--tblr-progress-bg:var(--tblr-border-color);--tblr-progress-border-radius:var(--tblr-border-radius);--tblr-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--tblr-progress-bar-color:#ffffff;--tblr-progress-bar-bg:var(--tblr-primary);--tblr-progress-bar-transition:width 0.6s ease;display:flex;height:var(--tblr-progress-height);overflow:hidden;font-size:var(--tblr-progress-font-size);background-color:var(--tblr-progress-bg);border-radius:var(--tblr-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--tblr-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--tblr-progress-bar-bg);transition:var(--tblr-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--tblr-progress-height) var(--tblr-progress-height)}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--tblr-list-group-color:#0f172a;--tblr-list-group-bg:inherit;--tblr-list-group-border-color:var(--tblr-border-color);--tblr-list-group-border-width:1px;--tblr-list-group-border-radius:4px;--tblr-list-group-item-padding-x:1.5rem;--tblr-list-group-item-padding-y:1rem;--tblr-list-group-action-color:inherit;--tblr-list-group-action-hover-color:inherit;--tblr-list-group-action-hover-bg:rgba(var(--tblr-muted-rgb), 0.04);--tblr-list-group-action-active-color:#1d273b;--tblr-list-group-action-active-bg:#e2e8f0;--tblr-list-group-disabled-color:#49566c;--tblr-list-group-disabled-bg:inherit;--tblr-list-group-active-color:inherit;--tblr-list-group-active-bg:var(--tblr-active-bg);--tblr-list-group-active-border-color:var(--tblr-border-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--tblr-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--tblr-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--tblr-list-group-action-hover-color);text-decoration:none;background-color:var(--tblr-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--tblr-list-group-action-active-color);background-color:var(--tblr-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--tblr-list-group-item-padding-y) var(--tblr-list-group-item-padding-x);color:var(--tblr-list-group-color);background-color:var(--tblr-list-group-bg);border:var(--tblr-list-group-border-width) solid var(--tblr-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--tblr-list-group-disabled-color);pointer-events:none;background-color:var(--tblr-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--tblr-list-group-active-color);background-color:var(--tblr-list-group-active-bg);border-color:var(--tblr-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--tblr-list-group-border-width));border-top-width:var(--tblr-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--tblr-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#134076;background-color:#d2e1f3}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#134076;background-color:#bdcbdb}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#134076;border-color:#134076}.list-group-item-secondary{color:#3a3e47;background-color:#dfe1e4}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#3a3e47;background-color:#c9cbcd}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#3a3e47;border-color:#3a3e47}.list-group-item-success{color:#1c6b29;background-color:#d5f0da}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#1c6b29;background-color:#c0d8c4}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#1c6b29;border-color:#1c6b29}.list-group-item-info{color:#285c87;background-color:#d9ebf9}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#285c87;background-color:#c3d4e0}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#285c87;border-color:#285c87}.list-group-item-warning{color:#943e04;background-color:#fde1cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#943e04;background-color:#e4cbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#943e04;border-color:#943e04}.list-group-item-danger{color:#802222;background-color:#f7d7d7}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#802222;background-color:#dec2c2}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#802222;border-color:#802222}.list-group-item-light{color:#959697;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#959697;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#959697;border-color:#959697}.list-group-item-dark{color:#111723;background-color:#d2d4d8}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#111723;background-color:#bdbfc2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#111723;border-color:#111723}.list-group-item-muted{color:#3a3e47;background-color:#dfe1e4}.list-group-item-muted.list-group-item-action:focus,.list-group-item-muted.list-group-item-action:hover{color:#3a3e47;background-color:#c9cbcd}.list-group-item-muted.list-group-item-action.active{color:#fff;background-color:#3a3e47;border-color:#3a3e47}.list-group-item-blue{color:#134076;background-color:#d2e1f3}.list-group-item-blue.list-group-item-action:focus,.list-group-item-blue.list-group-item-action:hover{color:#134076;background-color:#bdcbdb}.list-group-item-blue.list-group-item-action.active{color:#fff;background-color:#134076;border-color:#134076}.list-group-item-azure{color:#285c87;background-color:#d9ebf9}.list-group-item-azure.list-group-item-action:focus,.list-group-item-azure.list-group-item-action:hover{color:#285c87;background-color:#c3d4e0}.list-group-item-azure.list-group-item-action.active{color:#fff;background-color:#285c87;border-color:#285c87}.list-group-item-indigo{color:#283b8d;background-color:#d9e0fb}.list-group-item-indigo.list-group-item-action:focus,.list-group-item-indigo.list-group-item-action:hover{color:#283b8d;background-color:#c3cae2}.list-group-item-indigo.list-group-item-action.active{color:#fff;background-color:#283b8d;border-color:#283b8d}.list-group-item-purple{color:#682579;background-color:#efd8f4}.list-group-item-purple.list-group-item-action:focus,.list-group-item-purple.list-group-item-action:hover{color:#682579;background-color:#d7c2dc}.list-group-item-purple.list-group-item-action.active{color:#fff;background-color:#682579;border-color:#682579}.list-group-item-pink{color:#801f41;background-color:#f7d6e2}.list-group-item-pink.list-group-item-action:focus,.list-group-item-pink.list-group-item-action:hover{color:#801f41;background-color:#dec1cb}.list-group-item-pink.list-group-item-action.active{color:#fff;background-color:#801f41;border-color:#801f41}.list-group-item-red{color:#802222;background-color:#f7d7d7}.list-group-item-red.list-group-item-action:focus,.list-group-item-red.list-group-item-action:hover{color:#802222;background-color:#dec2c2}.list-group-item-red.list-group-item-action.active{color:#fff;background-color:#802222;border-color:#802222}.list-group-item-orange{color:#943e04;background-color:#fde1cd}.list-group-item-orange.list-group-item-action:focus,.list-group-item-orange.list-group-item-action:hover{color:#943e04;background-color:#e4cbb9}.list-group-item-orange.list-group-item-action.active{color:#fff;background-color:#943e04;border-color:#943e04}.list-group-item-yellow{color:#935f00;background-color:#fdeccc}.list-group-item-yellow.list-group-item-action:focus,.list-group-item-yellow.list-group-item-action:hover{color:#935f00;background-color:#e4d4b8}.list-group-item-yellow.list-group-item-action.active{color:#fff;background-color:#935f00;border-color:#935f00}.list-group-item-lime{color:#466e0d;background-color:#e3f1d0}.list-group-item-lime.list-group-item-action:focus,.list-group-item-lime.list-group-item-action:hover{color:#466e0d;background-color:#ccd9bb}.list-group-item-lime.list-group-item-action.active{color:#fff;background-color:#466e0d;border-color:#466e0d}.list-group-item-green{color:#1c6b29;background-color:#d5f0da}.list-group-item-green.list-group-item-action:focus,.list-group-item-green.list-group-item-action:hover{color:#1c6b29;background-color:#c0d8c4}.list-group-item-green.list-group-item-action.active{color:#fff;background-color:#1c6b29;border-color:#1c6b29}.list-group-item-teal{color:#076448;background-color:#ceede4}.list-group-item-teal.list-group-item-action:focus,.list-group-item-teal.list-group-item-action:hover{color:#076448;background-color:#b9d5cd}.list-group-item-teal.list-group-item-action.active{color:#fff;background-color:#076448;border-color:#076448}.list-group-item-cyan{color:#0e616e;background-color:#d1ecf1}.list-group-item-cyan.list-group-item-action:focus,.list-group-item-cyan.list-group-item-action:hover{color:#0e616e;background-color:#bcd4d9}.list-group-item-cyan.list-group-item-action.active{color:#fff;background-color:#0e616e;border-color:#0e616e}.list-group-item-facebook{color:#0e4791;background-color:#d1e4fc}.list-group-item-facebook.list-group-item-action:focus,.list-group-item-facebook.list-group-item-action:hover{color:#0e4791;background-color:#bccde3}.list-group-item-facebook.list-group-item-action.active{color:#fff;background-color:#0e4791;border-color:#0e4791}.list-group-item-twitter{color:#116191;background-color:#d2ecfc}.list-group-item-twitter.list-group-item-action:focus,.list-group-item-twitter.list-group-item-action:hover{color:#116191;background-color:#bdd4e3}.list-group-item-twitter.list-group-item-action.active{color:#fff;background-color:#116191;border-color:#116191}.list-group-item-linkedin{color:#063d74;background-color:#cee0f3}.list-group-item-linkedin.list-group-item-action:focus,.list-group-item-linkedin.list-group-item-action:hover{color:#063d74;background-color:#b9cadb}.list-group-item-linkedin.list-group-item-action.active{color:#fff;background-color:#063d74;border-color:#063d74}.list-group-item-google{color:#842f27;background-color:#f8dcd9}.list-group-item-google.list-group-item-action:focus,.list-group-item-google.list-group-item-action:hover{color:#842f27;background-color:#dfc6c3}.list-group-item-google.list-group-item-action.active{color:#fff;background-color:#842f27;border-color:#842f27}.list-group-item-youtube{color:#900;background-color:#fcc}.list-group-item-youtube.list-group-item-action:focus,.list-group-item-youtube.list-group-item-action:hover{color:#900;background-color:#e6b8b8}.list-group-item-youtube.list-group-item-action.active{color:#fff;background-color:#900;border-color:#900}.list-group-item-vimeo{color:#106e8c;background-color:#d1f1fb}.list-group-item-vimeo.list-group-item-action:focus,.list-group-item-vimeo.list-group-item-action:hover{color:#106e8c;background-color:#bcd9e2}.list-group-item-vimeo.list-group-item-action.active{color:#fff;background-color:#106e8c;border-color:#106e8c}.list-group-item-dribbble{color:#8c2e52;background-color:#fbdbe7}.list-group-item-dribbble.list-group-item-action:focus,.list-group-item-dribbble.list-group-item-action:hover{color:#8c2e52;background-color:#e2c5d0}.list-group-item-dribbble.list-group-item-action.active{color:#fff;background-color:#8c2e52;border-color:#8c2e52}.list-group-item-github{color:#0e0e0e;background-color:#d1d1d1}.list-group-item-github.list-group-item-action:focus,.list-group-item-github.list-group-item-action:hover{color:#0e0e0e;background-color:#bcbcbc}.list-group-item-github.list-group-item-action.active{color:#fff;background-color:#0e0e0e;border-color:#0e0e0e}.list-group-item-instagram{color:#892639;background-color:#fad9df}.list-group-item-instagram.list-group-item-action:focus,.list-group-item-instagram.list-group-item-action:hover{color:#892639;background-color:#e1c3c9}.list-group-item-instagram.list-group-item-action.active{color:#fff;background-color:#892639;border-color:#892639}.list-group-item-pinterest{color:#710511;background-color:#f2ced2}.list-group-item-pinterest.list-group-item-action:focus,.list-group-item-pinterest.list-group-item-action:hover{color:#710511;background-color:#dab9bd}.list-group-item-pinterest.list-group-item-action.active{color:#fff;background-color:#710511;border-color:#710511}.list-group-item-vk{color:#3b4f65;background-color:#e0e6ee}.list-group-item-vk.list-group-item-action:focus,.list-group-item-vk.list-group-item-action:hover{color:#3b4f65;background-color:#cacfd6}.list-group-item-vk.list-group-item-action.active{color:#fff;background-color:#3b4f65;border-color:#3b4f65}.list-group-item-rss{color:#996300;background-color:#ffedcc}.list-group-item-rss.list-group-item-action:focus,.list-group-item-rss.list-group-item-action:hover{color:#996300;background-color:#e6d5b8}.list-group-item-rss.list-group-item-action.active{color:#fff;background-color:#996300;border-color:#996300}.list-group-item-flickr{color:#003b84;background-color:#cce0f8}.list-group-item-flickr.list-group-item-action:focus,.list-group-item-flickr.list-group-item-action:hover{color:#003b84;background-color:#b8cadf}.list-group-item-flickr.list-group-item-action.active{color:#fff;background-color:#003b84;border-color:#003b84}.list-group-item-bitbucket{color:#00317a;background-color:#ccdcf5}.list-group-item-bitbucket.list-group-item-action:focus,.list-group-item-bitbucket.list-group-item-action:hover{color:#00317a;background-color:#b8c6dd}.list-group-item-bitbucket.list-group-item-action.active{color:#fff;background-color:#00317a;border-color:#00317a}.list-group-item-tabler{color:#134076;background-color:#d2e1f3}.list-group-item-tabler.list-group-item-action:focus,.list-group-item-tabler.list-group-item-action:hover{color:#134076;background-color:#bdcbdb}.list-group-item-tabler.list-group-item-action.active{color:#fff;background-color:#134076;border-color:#134076}.btn-close{box-sizing:content-box;width:.75rem;height:.75rem;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/.75rem auto no-repeat;border:0;border-radius:4px;opacity:.3}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(32,107,196,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--tblr-toast-zindex:1090;--tblr-toast-padding-x:0.75rem;--tblr-toast-padding-y:0.5rem;--tblr-toast-spacing:1.5rem;--tblr-toast-max-width:350px;--tblr-toast-font-size:0.875rem;--tblr-toast-bg:rgba(255, 255, 255, 0.85);--tblr-toast-border-width:1px;--tblr-toast-border-color:var(--tblr-border-color);--tblr-toast-border-radius:4px;--tblr-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--tblr-toast-header-color:var(--tblr-muted);--tblr-toast-header-bg:rgba(255, 255, 255, 0.85);--tblr-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--tblr-toast-max-width);max-width:100%;font-size:var(--tblr-toast-font-size);color:var(--tblr-toast-color);pointer-events:auto;background-color:var(--tblr-toast-bg);background-clip:padding-box;border:var(--tblr-toast-border-width) solid var(--tblr-toast-border-color);box-shadow:var(--tblr-toast-box-shadow);border-radius:var(--tblr-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--tblr-toast-zindex:1090;position:absolute;z-index:var(--tblr-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--tblr-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--tblr-toast-padding-y) var(--tblr-toast-padding-x);color:var(--tblr-toast-header-color);background-color:var(--tblr-toast-header-bg);background-clip:padding-box;border-bottom:var(--tblr-toast-border-width) solid var(--tblr-toast-header-border-color);border-top-left-radius:calc(var(--tblr-toast-border-radius) - var(--tblr-toast-border-width));border-top-right-radius:calc(var(--tblr-toast-border-radius) - var(--tblr-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--tblr-toast-padding-x));margin-left:var(--tblr-toast-padding-x)}.toast-body{padding:var(--tblr-toast-padding-x);word-wrap:break-word}.modal{--tblr-modal-zindex:1055;--tblr-modal-width:540px;--tblr-modal-padding:1.5rem;--tblr-modal-margin:0.5rem;--tblr-modal-bg:var(--tblr-bg-surface);--tblr-modal-border-color:transparent;--tblr-modal-border-width:1px;--tblr-modal-border-radius:var(--tblr-border-radius-lg);--tblr-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--tblr-modal-inner-border-radius:calc(var(--tblr-modal-border-radius) - 1px);--tblr-modal-header-padding-x:1.5rem;--tblr-modal-header-padding-y:1.5rem;--tblr-modal-header-padding:1.5rem;--tblr-modal-header-border-color:var(--tblr-border-color);--tblr-modal-header-border-width:1px;--tblr-modal-title-line-height:1.4285714286;--tblr-modal-footer-gap:0.75rem;--tblr-modal-footer-border-color:var(--tblr-border-color);--tblr-modal-footer-border-width:0;position:fixed;top:0;left:0;z-index:var(--tblr-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--tblr-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-1rem)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--tblr-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--tblr-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--tblr-modal-color);pointer-events:auto;background-color:var(--tblr-modal-bg);background-clip:padding-box;border:var(--tblr-modal-border-width) solid var(--tblr-modal-border-color);border-radius:var(--tblr-modal-border-radius);outline:0}.modal-backdrop{--tblr-backdrop-zindex:1050;--tblr-backdrop-bg:#1d273b;--tblr-backdrop-opacity:0.24;position:fixed;top:0;left:0;z-index:var(--tblr-backdrop-zindex);width:100vw;height:100vh;background-color:var(--tblr-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--tblr-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--tblr-modal-header-padding);border-bottom:var(--tblr-modal-header-border-width) solid var(--tblr-modal-header-border-color);border-top-left-radius:var(--tblr-modal-inner-border-radius);border-top-right-radius:var(--tblr-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--tblr-modal-header-padding-y) * .5) calc(var(--tblr-modal-header-padding-x) * .5);margin:calc(-.5 * var(--tblr-modal-header-padding-y)) calc(-.5 * var(--tblr-modal-header-padding-x)) calc(-.5 * var(--tblr-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--tblr-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--tblr-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--tblr-modal-padding) - var(--tblr-modal-footer-gap) * .5);background-color:var(--tblr-modal-footer-bg);border-top:var(--tblr-modal-footer-border-width) solid var(--tblr-modal-footer-border-color);border-bottom-right-radius:var(--tblr-modal-inner-border-radius);border-bottom-left-radius:var(--tblr-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--tblr-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--tblr-modal-margin:1.75rem;--tblr-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--tblr-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--tblr-modal-width:380px}}@media (min-width:992px){.modal-lg,.modal-xl{--tblr-modal-width:720px}}@media (min-width:1200px){.modal-xl{--tblr-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--tblr-tooltip-zindex:1080;--tblr-tooltip-max-width:200px;--tblr-tooltip-padding-x:0.5rem;--tblr-tooltip-padding-y:0.25rem;--tblr-tooltip-font-size:0.765625rem;--tblr-tooltip-color:var(--tblr-light);--tblr-tooltip-bg:var(--tblr-bg-surface-dark);--tblr-tooltip-border-radius:4px;--tblr-tooltip-opacity:0.9;--tblr-tooltip-arrow-width:0.8rem;--tblr-tooltip-arrow-height:0.4rem;z-index:var(--tblr-tooltip-zindex);display:block;padding:var(--tblr-tooltip-arrow-height);margin:var(--tblr-tooltip-margin);font-family:var(--tblr-font-sans-serif);font-style:normal;font-weight:400;line-height:1.4285714286;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--tblr-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--tblr-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--tblr-tooltip-arrow-width);height:var(--tblr-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--tblr-tooltip-arrow-height) calc(var(--tblr-tooltip-arrow-width) * .5) 0;border-top-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height) calc(var(--tblr-tooltip-arrow-width) * .5) 0;border-right-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height);border-bottom-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--tblr-tooltip-arrow-width) * .5) 0 calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height);border-left-color:var(--tblr-tooltip-bg)}.tooltip-inner{max-width:var(--tblr-tooltip-max-width);padding:var(--tblr-tooltip-padding-y) var(--tblr-tooltip-padding-x);color:var(--tblr-tooltip-color);text-align:center;background-color:var(--tblr-tooltip-bg);border-radius:var(--tblr-tooltip-border-radius)}.popover{--tblr-popover-zindex:1070;--tblr-popover-max-width:276px;--tblr-popover-font-size:0.765625rem;--tblr-popover-bg:var(--tblr-bg-surface);--tblr-popover-border-width:1px;--tblr-popover-border-color:var(--tblr-border-color);--tblr-popover-border-radius:8px;--tblr-popover-inner-border-radius:7px;--tblr-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--tblr-popover-header-padding-x:1rem;--tblr-popover-header-padding-y:0.5rem;--tblr-popover-header-font-size:0.875rem;--tblr-popover-header-bg:transparent;--tblr-popover-body-padding-x:1rem;--tblr-popover-body-padding-y:1rem;--tblr-popover-body-color:inherit;--tblr-popover-arrow-width:1rem;--tblr-popover-arrow-height:0.5rem;--tblr-popover-arrow-border:var(--tblr-popover-border-color);z-index:var(--tblr-popover-zindex);display:block;max-width:var(--tblr-popover-max-width);font-family:var(--tblr-font-sans-serif);font-style:normal;font-weight:400;line-height:1.4285714286;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--tblr-popover-font-size);word-wrap:break-word;background-color:var(--tblr-popover-bg);background-clip:padding-box;border:var(--tblr-popover-border-width) solid var(--tblr-popover-border-color);border-radius:var(--tblr-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--tblr-popover-arrow-width);height:var(--tblr-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--tblr-popover-border-width);border-top-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width));width:var(--tblr-popover-arrow-height);height:var(--tblr-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--tblr-popover-border-width);border-right-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--tblr-popover-border-width);border-bottom-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--tblr-popover-arrow-width);margin-left:calc(-.5 * var(--tblr-popover-arrow-width));content:"";border-bottom:var(--tblr-popover-border-width) solid var(--tblr-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width));width:var(--tblr-popover-arrow-height);height:var(--tblr-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--tblr-popover-arrow-width) * .5) 0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--tblr-popover-border-width);border-left-color:var(--tblr-popover-bg)}.popover-header{padding:var(--tblr-popover-header-padding-y) var(--tblr-popover-header-padding-x);margin-bottom:0;font-size:var(--tblr-popover-header-font-size);color:var(--tblr-popover-header-color);background-color:var(--tblr-popover-header-bg);border-bottom:var(--tblr-popover-border-width) solid var(--tblr-popover-border-color);border-top-left-radius:var(--tblr-popover-inner-border-radius);border-top-right-radius:var(--tblr-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--tblr-popover-body-padding-y) var(--tblr-popover-body-padding-x);color:var(--tblr-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:1.5rem;height:1.5rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='15 18 9 12 15 6'%3e%3c/polyline%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='9 18 15 12 9 6'%3e%3c/polyline%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--tblr-spinner-width);height:var(--tblr-spinner-height);vertical-align:var(--tblr-spinner-vertical-align);border-radius:50%;animation:var(--tblr-spinner-animation-speed) linear infinite var(--tblr-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--tblr-spinner-width:1.5rem;--tblr-spinner-height:1.5rem;--tblr-spinner-vertical-align:-0.125em;--tblr-spinner-border-width:2px;--tblr-spinner-animation-speed:0.75s;--tblr-spinner-animation-name:spinner-border;border:var(--tblr-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--tblr-spinner-width:1rem;--tblr-spinner-height:1rem;--tblr-spinner-border-width:1px}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--tblr-spinner-width:1.5rem;--tblr-spinner-height:1.5rem;--tblr-spinner-vertical-align:-0.125em;--tblr-spinner-animation-speed:0.75s;--tblr-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--tblr-spinner-width:1rem;--tblr-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--tblr-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--tblr-offcanvas-zindex:1045;--tblr-offcanvas-width:400px;--tblr-offcanvas-height:30vh;--tblr-offcanvas-padding-x:1.5rem;--tblr-offcanvas-padding-y:1.5rem;--tblr-offcanvas-bg:var(--tblr-bg-surface);--tblr-offcanvas-border-width:1px;--tblr-offcanvas-border-color:var(--tblr-border-color);--tblr-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#1d273b}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.24}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tblr-offcanvas-padding-y) var(--tblr-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--tblr-offcanvas-padding-y) * .5) calc(var(--tblr-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--tblr-offcanvas-padding-y));margin-right:calc(-.5 * var(--tblr-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--tblr-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:1.4285714286}.offcanvas-body{flex-grow:1;padding:var(--tblr-offcanvas-padding-y) var(--tblr-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.2}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.1}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.9) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.9) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0;mask-position:-200% 0}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#f8fafc!important;background-color:RGBA(32,107,196,var(--tblr-bg-opacity,1))!important}.text-bg-secondary{color:#f8fafc!important;background-color:RGBA(97,104,118,var(--tblr-bg-opacity,1))!important}.text-bg-success{color:#f8fafc!important;background-color:RGBA(47,179,68,var(--tblr-bg-opacity,1))!important}.text-bg-info{color:#f8fafc!important;background-color:RGBA(66,153,225,var(--tblr-bg-opacity,1))!important}.text-bg-warning{color:#f8fafc!important;background-color:RGBA(247,103,7,var(--tblr-bg-opacity,1))!important}.text-bg-danger{color:#f8fafc!important;background-color:RGBA(214,57,57,var(--tblr-bg-opacity,1))!important}.text-bg-light{color:#1d273b!important;background-color:RGBA(248,250,252,var(--tblr-bg-opacity,1))!important}.text-bg-dark{color:#f8fafc!important;background-color:RGBA(29,39,59,var(--tblr-bg-opacity,1))!important}.text-bg-muted{color:#f8fafc!important;background-color:RGBA(97,104,118,var(--tblr-bg-opacity,1))!important}.text-bg-blue{color:#f8fafc!important;background-color:RGBA(32,107,196,var(--tblr-bg-opacity,1))!important}.text-bg-azure{color:#f8fafc!important;background-color:RGBA(66,153,225,var(--tblr-bg-opacity,1))!important}.text-bg-indigo{color:#f8fafc!important;background-color:RGBA(66,99,235,var(--tblr-bg-opacity,1))!important}.text-bg-purple{color:#f8fafc!important;background-color:RGBA(174,62,201,var(--tblr-bg-opacity,1))!important}.text-bg-pink{color:#f8fafc!important;background-color:RGBA(214,51,108,var(--tblr-bg-opacity,1))!important}.text-bg-red{color:#f8fafc!important;background-color:RGBA(214,57,57,var(--tblr-bg-opacity,1))!important}.text-bg-orange{color:#f8fafc!important;background-color:RGBA(247,103,7,var(--tblr-bg-opacity,1))!important}.text-bg-yellow{color:#f8fafc!important;background-color:RGBA(245,159,0,var(--tblr-bg-opacity,1))!important}.text-bg-lime{color:#f8fafc!important;background-color:RGBA(116,184,22,var(--tblr-bg-opacity,1))!important}.text-bg-green{color:#f8fafc!important;background-color:RGBA(47,179,68,var(--tblr-bg-opacity,1))!important}.text-bg-teal{color:#f8fafc!important;background-color:RGBA(12,166,120,var(--tblr-bg-opacity,1))!important}.text-bg-cyan{color:#f8fafc!important;background-color:RGBA(23,162,184,var(--tblr-bg-opacity,1))!important}.text-bg-facebook{color:#f8fafc!important;background-color:RGBA(24,119,242,var(--tblr-bg-opacity,1))!important}.text-bg-twitter{color:#f8fafc!important;background-color:RGBA(29,161,242,var(--tblr-bg-opacity,1))!important}.text-bg-linkedin{color:#f8fafc!important;background-color:RGBA(10,102,194,var(--tblr-bg-opacity,1))!important}.text-bg-google{color:#f8fafc!important;background-color:RGBA(220,78,65,var(--tblr-bg-opacity,1))!important}.text-bg-youtube{color:#f8fafc!important;background-color:RGBA(255,0,0,var(--tblr-bg-opacity,1))!important}.text-bg-vimeo{color:#f8fafc!important;background-color:RGBA(26,183,234,var(--tblr-bg-opacity,1))!important}.text-bg-dribbble{color:#f8fafc!important;background-color:RGBA(234,76,137,var(--tblr-bg-opacity,1))!important}.text-bg-github{color:#f8fafc!important;background-color:RGBA(24,23,23,var(--tblr-bg-opacity,1))!important}.text-bg-instagram{color:#f8fafc!important;background-color:RGBA(228,64,95,var(--tblr-bg-opacity,1))!important}.text-bg-pinterest{color:#f8fafc!important;background-color:RGBA(189,8,28,var(--tblr-bg-opacity,1))!important}.text-bg-vk{color:#f8fafc!important;background-color:RGBA(99,131,168,var(--tblr-bg-opacity,1))!important}.text-bg-rss{color:#f8fafc!important;background-color:RGBA(255,165,0,var(--tblr-bg-opacity,1))!important}.text-bg-flickr{color:#f8fafc!important;background-color:RGBA(0,99,220,var(--tblr-bg-opacity,1))!important}.text-bg-bitbucket{color:#f8fafc!important;background-color:RGBA(0,82,204,var(--tblr-bg-opacity,1))!important}.text-bg-tabler{color:#f8fafc!important;background-color:RGBA(32,107,196,var(--tblr-bg-opacity,1))!important}.link-primary{color:#206bc4!important}.link-primary:focus,.link-primary:hover{color:#1a569d!important}.link-secondary{color:#616876!important}.link-secondary:focus,.link-secondary:hover{color:#4e535e!important}.link-success{color:#2fb344!important}.link-success:focus,.link-success:hover{color:#268f36!important}.link-info{color:#4299e1!important}.link-info:focus,.link-info:hover{color:#357ab4!important}.link-warning{color:#f76707!important}.link-warning:focus,.link-warning:hover{color:#c65206!important}.link-danger{color:#d63939!important}.link-danger:focus,.link-danger:hover{color:#ab2e2e!important}.link-light{color:#f8fafc!important}.link-light:focus,.link-light:hover{color:#f9fbfd!important}.link-dark{color:#1d273b!important}.link-dark:focus,.link-dark:hover{color:#171f2f!important}.link-muted{color:#616876!important}.link-muted:focus,.link-muted:hover{color:#4e535e!important}.link-blue{color:#206bc4!important}.link-blue:focus,.link-blue:hover{color:#1a569d!important}.link-azure{color:#4299e1!important}.link-azure:focus,.link-azure:hover{color:#357ab4!important}.link-indigo{color:#4263eb!important}.link-indigo:focus,.link-indigo:hover{color:#354fbc!important}.link-purple{color:#ae3ec9!important}.link-purple:focus,.link-purple:hover{color:#8b32a1!important}.link-pink{color:#d6336c!important}.link-pink:focus,.link-pink:hover{color:#ab2956!important}.link-red{color:#d63939!important}.link-red:focus,.link-red:hover{color:#ab2e2e!important}.link-orange{color:#f76707!important}.link-orange:focus,.link-orange:hover{color:#c65206!important}.link-yellow{color:#f59f00!important}.link-yellow:focus,.link-yellow:hover{color:#c47f00!important}.link-lime{color:#74b816!important}.link-lime:focus,.link-lime:hover{color:#5d9312!important}.link-green{color:#2fb344!important}.link-green:focus,.link-green:hover{color:#268f36!important}.link-teal{color:#0ca678!important}.link-teal:focus,.link-teal:hover{color:#0a8560!important}.link-cyan{color:#17a2b8!important}.link-cyan:focus,.link-cyan:hover{color:#128293!important}.link-facebook{color:#1877f2!important}.link-facebook:focus,.link-facebook:hover{color:#135fc2!important}.link-twitter{color:#1da1f2!important}.link-twitter:focus,.link-twitter:hover{color:#1781c2!important}.link-linkedin{color:#0a66c2!important}.link-linkedin:focus,.link-linkedin:hover{color:#08529b!important}.link-google{color:#dc4e41!important}.link-google:focus,.link-google:hover{color:#b03e34!important}.link-youtube{color:red!important}.link-youtube:focus,.link-youtube:hover{color:#c00!important}.link-vimeo{color:#1ab7ea!important}.link-vimeo:focus,.link-vimeo:hover{color:#1592bb!important}.link-dribbble{color:#ea4c89!important}.link-dribbble:focus,.link-dribbble:hover{color:#bb3d6e!important}.link-github{color:#181717!important}.link-github:focus,.link-github:hover{color:#131212!important}.link-instagram{color:#e4405f!important}.link-instagram:focus,.link-instagram:hover{color:#b6334c!important}.link-pinterest{color:#bd081c!important}.link-pinterest:focus,.link-pinterest:hover{color:#970616!important}.link-vk{color:#6383a8!important}.link-vk:focus,.link-vk:hover{color:#4f6986!important}.link-rss{color:orange!important}.link-rss:focus,.link-rss:hover{color:#cc8400!important}.link-flickr{color:#0063dc!important}.link-flickr:focus,.link-flickr:hover{color:#004fb0!important}.link-bitbucket{color:#0052cc!important}.link-bitbucket:focus,.link-bitbucket:hover{color:#0042a3!important}.link-tabler{color:#206bc4!important}.link-tabler:focus,.link-tabler:hover{color:#1a569d!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--tblr-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--tblr-aspect-ratio:100%}.ratio-2x1{--tblr-aspect-ratio:50%}.ratio-1x2{--tblr-aspect-ratio:200%}.ratio-3x1{--tblr-aspect-ratio:33.3333333333%}.ratio-1x3{--tblr-aspect-ratio:300%}.ratio-4x3{--tblr-aspect-ratio:75%}.ratio-3x4{--tblr-aspect-ratio:133.3333333333%}.ratio-16x9{--tblr-aspect-ratio:56.25%}.ratio-9x16{--tblr-aspect-ratio:177.7777777778%}.ratio-21x9{--tblr-aspect-ratio:42.8571428571%}.ratio-9x21{--tblr-aspect-ratio:233.3333333333%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.16}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-wide{border:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-0{border:0!important}.border-top{border-top:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-top-wide{border-top:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-top-0{border-top:0!important}.border-end{border-right:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-end-wide{border-right:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-bottom-wide{border-bottom:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-start-wide{border-left:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-start-0{border-left:0!important}.border-primary{--tblr-border-opacity:1;border-color:rgba(var(--tblr-primary-rgb),var(--tblr-border-opacity))!important}.border-secondary{--tblr-border-opacity:1;border-color:rgba(var(--tblr-secondary-rgb),var(--tblr-border-opacity))!important}.border-success{--tblr-border-opacity:1;border-color:rgba(var(--tblr-success-rgb),var(--tblr-border-opacity))!important}.border-info{--tblr-border-opacity:1;border-color:rgba(var(--tblr-info-rgb),var(--tblr-border-opacity))!important}.border-warning{--tblr-border-opacity:1;border-color:rgba(var(--tblr-warning-rgb),var(--tblr-border-opacity))!important}.border-danger{--tblr-border-opacity:1;border-color:rgba(var(--tblr-danger-rgb),var(--tblr-border-opacity))!important}.border-light{--tblr-border-opacity:1;border-color:rgba(var(--tblr-light-rgb),var(--tblr-border-opacity))!important}.border-dark{--tblr-border-opacity:1;border-color:rgba(var(--tblr-dark-rgb),var(--tblr-border-opacity))!important}.border-muted{--tblr-border-opacity:1;border-color:rgba(var(--tblr-muted-rgb),var(--tblr-border-opacity))!important}.border-blue{--tblr-border-opacity:1;border-color:rgba(var(--tblr-blue-rgb),var(--tblr-border-opacity))!important}.border-azure{--tblr-border-opacity:1;border-color:rgba(var(--tblr-azure-rgb),var(--tblr-border-opacity))!important}.border-indigo{--tblr-border-opacity:1;border-color:rgba(var(--tblr-indigo-rgb),var(--tblr-border-opacity))!important}.border-purple{--tblr-border-opacity:1;border-color:rgba(var(--tblr-purple-rgb),var(--tblr-border-opacity))!important}.border-pink{--tblr-border-opacity:1;border-color:rgba(var(--tblr-pink-rgb),var(--tblr-border-opacity))!important}.border-red{--tblr-border-opacity:1;border-color:rgba(var(--tblr-red-rgb),var(--tblr-border-opacity))!important}.border-orange{--tblr-border-opacity:1;border-color:rgba(var(--tblr-orange-rgb),var(--tblr-border-opacity))!important}.border-yellow{--tblr-border-opacity:1;border-color:rgba(var(--tblr-yellow-rgb),var(--tblr-border-opacity))!important}.border-lime{--tblr-border-opacity:1;border-color:rgba(var(--tblr-lime-rgb),var(--tblr-border-opacity))!important}.border-green{--tblr-border-opacity:1;border-color:rgba(var(--tblr-green-rgb),var(--tblr-border-opacity))!important}.border-teal{--tblr-border-opacity:1;border-color:rgba(var(--tblr-teal-rgb),var(--tblr-border-opacity))!important}.border-cyan{--tblr-border-opacity:1;border-color:rgba(var(--tblr-cyan-rgb),var(--tblr-border-opacity))!important}.border-facebook{--tblr-border-opacity:1;border-color:rgba(var(--tblr-facebook-rgb),var(--tblr-border-opacity))!important}.border-twitter{--tblr-border-opacity:1;border-color:rgba(var(--tblr-twitter-rgb),var(--tblr-border-opacity))!important}.border-linkedin{--tblr-border-opacity:1;border-color:rgba(var(--tblr-linkedin-rgb),var(--tblr-border-opacity))!important}.border-google{--tblr-border-opacity:1;border-color:rgba(var(--tblr-google-rgb),var(--tblr-border-opacity))!important}.border-youtube{--tblr-border-opacity:1;border-color:rgba(var(--tblr-youtube-rgb),var(--tblr-border-opacity))!important}.border-vimeo{--tblr-border-opacity:1;border-color:rgba(var(--tblr-vimeo-rgb),var(--tblr-border-opacity))!important}.border-dribbble{--tblr-border-opacity:1;border-color:rgba(var(--tblr-dribbble-rgb),var(--tblr-border-opacity))!important}.border-github{--tblr-border-opacity:1;border-color:rgba(var(--tblr-github-rgb),var(--tblr-border-opacity))!important}.border-instagram{--tblr-border-opacity:1;border-color:rgba(var(--tblr-instagram-rgb),var(--tblr-border-opacity))!important}.border-pinterest{--tblr-border-opacity:1;border-color:rgba(var(--tblr-pinterest-rgb),var(--tblr-border-opacity))!important}.border-vk{--tblr-border-opacity:1;border-color:rgba(var(--tblr-vk-rgb),var(--tblr-border-opacity))!important}.border-rss{--tblr-border-opacity:1;border-color:rgba(var(--tblr-rss-rgb),var(--tblr-border-opacity))!important}.border-flickr{--tblr-border-opacity:1;border-color:rgba(var(--tblr-flickr-rgb),var(--tblr-border-opacity))!important}.border-bitbucket{--tblr-border-opacity:1;border-color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-border-opacity))!important}.border-tabler{--tblr-border-opacity:1;border-color:rgba(var(--tblr-tabler-rgb),var(--tblr-border-opacity))!important}.border-white{--tblr-border-opacity:1;border-color:rgba(var(--tblr-white-rgb),var(--tblr-border-opacity))!important}.border-1{--tblr-border-width:1px}.border-2{--tblr-border-width:2px}.border-3{--tblr-border-width:3px}.border-4{--tblr-border-width:4px}.border-5{--tblr-border-width:5px}.border-opacity-10{--tblr-border-opacity:0.1}.border-opacity-25{--tblr-border-opacity:0.25}.border-opacity-50{--tblr-border-opacity:0.5}.border-opacity-75{--tblr-border-opacity:0.75}.border-opacity-100{--tblr-border-opacity:1}.w-0{width:0!important}.w-1{width:.25rem!important}.w-2{width:.5rem!important}.w-3{width:1rem!important}.w-4{width:2rem!important}.w-5{width:4rem!important}.w-25{width:25%!important}.w-33{width:33.33333%!important}.w-50{width:50%!important}.w-66{width:66.66666%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-0{height:0!important}.h-1{height:.25rem!important}.h-2{height:.5rem!important}.h-3{height:1rem!important}.h-4{height:2rem!important}.h-5{height:4rem!important}.h-25{height:25%!important}.h-33{height:33.33333%!important}.h-50{height:50%!important}.h-66{height:66.66666%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:2rem!important}.m-5{margin:4rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:2rem!important;margin-left:2rem!important}.mx-5{margin-right:4rem!important;margin-left:4rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:2rem!important}.mt-5{margin-top:4rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:2rem!important}.me-5{margin-right:4rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:2rem!important}.mb-5{margin-bottom:4rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:2rem!important}.ms-5{margin-left:4rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:2rem!important}.p-5{padding:4rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:2rem!important;padding-left:2rem!important}.px-5{padding-right:4rem!important;padding-left:4rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:2rem!important}.pt-5{padding-top:4rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:2rem!important}.pe-5{padding-right:4rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:2rem!important}.pb-5{padding-bottom:4rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:2rem!important}.ps-5{padding-left:4rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:2rem!important}.gap-5{gap:4rem!important}.font-monospace{font-family:var(--tblr-font-monospace)!important}.fs-1{font-size:1.5rem!important}.fs-2{font-size:1.25rem!important}.fs-3{font-size:1rem!important}.fs-4{font-size:.875rem!important}.fs-5{font-size:.75rem!important}.fs-6{font-size:.625rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:600!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.1428571429!important}.lh-base{line-height:1.4285714286!important}.lh-lg{line-height:1.7142857143!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--tblr-text-opacity:1;color:rgba(var(--tblr-primary-rgb),var(--tblr-text-opacity))!important}.text-secondary{--tblr-text-opacity:1;color:rgba(var(--tblr-secondary-rgb),var(--tblr-text-opacity))!important}.text-success{--tblr-text-opacity:1;color:rgba(var(--tblr-success-rgb),var(--tblr-text-opacity))!important}.text-info{--tblr-text-opacity:1;color:rgba(var(--tblr-info-rgb),var(--tblr-text-opacity))!important}.text-warning{--tblr-text-opacity:1;color:rgba(var(--tblr-warning-rgb),var(--tblr-text-opacity))!important}.text-danger{--tblr-text-opacity:1;color:rgba(var(--tblr-danger-rgb),var(--tblr-text-opacity))!important}.text-light{--tblr-text-opacity:1;color:rgba(var(--tblr-light-rgb),var(--tblr-text-opacity))!important}.text-dark{--tblr-text-opacity:1;color:rgba(var(--tblr-dark-rgb),var(--tblr-text-opacity))!important}.text-muted{--tblr-text-opacity:1;color:#616876!important}.text-blue{--tblr-text-opacity:1;color:rgba(var(--tblr-blue-rgb),var(--tblr-text-opacity))!important}.text-azure{--tblr-text-opacity:1;color:rgba(var(--tblr-azure-rgb),var(--tblr-text-opacity))!important}.text-indigo{--tblr-text-opacity:1;color:rgba(var(--tblr-indigo-rgb),var(--tblr-text-opacity))!important}.text-purple{--tblr-text-opacity:1;color:rgba(var(--tblr-purple-rgb),var(--tblr-text-opacity))!important}.text-pink{--tblr-text-opacity:1;color:rgba(var(--tblr-pink-rgb),var(--tblr-text-opacity))!important}.text-red{--tblr-text-opacity:1;color:rgba(var(--tblr-red-rgb),var(--tblr-text-opacity))!important}.text-orange{--tblr-text-opacity:1;color:rgba(var(--tblr-orange-rgb),var(--tblr-text-opacity))!important}.text-yellow{--tblr-text-opacity:1;color:rgba(var(--tblr-yellow-rgb),var(--tblr-text-opacity))!important}.text-lime{--tblr-text-opacity:1;color:rgba(var(--tblr-lime-rgb),var(--tblr-text-opacity))!important}.text-green{--tblr-text-opacity:1;color:rgba(var(--tblr-green-rgb),var(--tblr-text-opacity))!important}.text-teal{--tblr-text-opacity:1;color:rgba(var(--tblr-teal-rgb),var(--tblr-text-opacity))!important}.text-cyan{--tblr-text-opacity:1;color:rgba(var(--tblr-cyan-rgb),var(--tblr-text-opacity))!important}.text-facebook{--tblr-text-opacity:1;color:rgba(var(--tblr-facebook-rgb),var(--tblr-text-opacity))!important}.text-twitter{--tblr-text-opacity:1;color:rgba(var(--tblr-twitter-rgb),var(--tblr-text-opacity))!important}.text-linkedin{--tblr-text-opacity:1;color:rgba(var(--tblr-linkedin-rgb),var(--tblr-text-opacity))!important}.text-google{--tblr-text-opacity:1;color:rgba(var(--tblr-google-rgb),var(--tblr-text-opacity))!important}.text-youtube{--tblr-text-opacity:1;color:rgba(var(--tblr-youtube-rgb),var(--tblr-text-opacity))!important}.text-vimeo{--tblr-text-opacity:1;color:rgba(var(--tblr-vimeo-rgb),var(--tblr-text-opacity))!important}.text-dribbble{--tblr-text-opacity:1;color:rgba(var(--tblr-dribbble-rgb),var(--tblr-text-opacity))!important}.text-github{--tblr-text-opacity:1;color:rgba(var(--tblr-github-rgb),var(--tblr-text-opacity))!important}.text-instagram{--tblr-text-opacity:1;color:rgba(var(--tblr-instagram-rgb),var(--tblr-text-opacity))!important}.text-pinterest{--tblr-text-opacity:1;color:rgba(var(--tblr-pinterest-rgb),var(--tblr-text-opacity))!important}.text-vk{--tblr-text-opacity:1;color:rgba(var(--tblr-vk-rgb),var(--tblr-text-opacity))!important}.text-rss{--tblr-text-opacity:1;color:rgba(var(--tblr-rss-rgb),var(--tblr-text-opacity))!important}.text-flickr{--tblr-text-opacity:1;color:rgba(var(--tblr-flickr-rgb),var(--tblr-text-opacity))!important}.text-bitbucket{--tblr-text-opacity:1;color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-text-opacity))!important}.text-tabler{--tblr-text-opacity:1;color:rgba(var(--tblr-tabler-rgb),var(--tblr-text-opacity))!important}.text-black{--tblr-text-opacity:1;color:rgba(var(--tblr-black-rgb),var(--tblr-text-opacity))!important}.text-white{--tblr-text-opacity:1;color:rgba(var(--tblr-white-rgb),var(--tblr-text-opacity))!important}.text-body{--tblr-text-opacity:1;color:rgba(var(--tblr-body-color-rgb),var(--tblr-text-opacity))!important}.text-black-50{--tblr-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--tblr-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--tblr-text-opacity:1;color:inherit!important}.text-opacity-25{--tblr-text-opacity:0.25}.text-opacity-50{--tblr-text-opacity:0.5}.text-opacity-75{--tblr-text-opacity:0.75}.text-opacity-100{--tblr-text-opacity:1}.bg-primary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-primary-rgb),var(--tblr-bg-opacity))!important}.bg-secondary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-secondary-rgb),var(--tblr-bg-opacity))!important}.bg-success{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-success-rgb),var(--tblr-bg-opacity))!important}.bg-info{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-info-rgb),var(--tblr-bg-opacity))!important}.bg-warning{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-warning-rgb),var(--tblr-bg-opacity))!important}.bg-danger{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-danger-rgb),var(--tblr-bg-opacity))!important}.bg-light{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-light-rgb),var(--tblr-bg-opacity))!important}.bg-dark{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-dark-rgb),var(--tblr-bg-opacity))!important}.bg-muted{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-muted-rgb),var(--tblr-bg-opacity))!important}.bg-blue{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-blue-rgb),var(--tblr-bg-opacity))!important}.bg-azure{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-azure-rgb),var(--tblr-bg-opacity))!important}.bg-indigo{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-indigo-rgb),var(--tblr-bg-opacity))!important}.bg-purple{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-purple-rgb),var(--tblr-bg-opacity))!important}.bg-pink{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-pink-rgb),var(--tblr-bg-opacity))!important}.bg-red{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-red-rgb),var(--tblr-bg-opacity))!important}.bg-orange{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-orange-rgb),var(--tblr-bg-opacity))!important}.bg-yellow{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-yellow-rgb),var(--tblr-bg-opacity))!important}.bg-lime{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-lime-rgb),var(--tblr-bg-opacity))!important}.bg-green{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-green-rgb),var(--tblr-bg-opacity))!important}.bg-teal{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-teal-rgb),var(--tblr-bg-opacity))!important}.bg-cyan{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-cyan-rgb),var(--tblr-bg-opacity))!important}.bg-facebook{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-facebook-rgb),var(--tblr-bg-opacity))!important}.bg-twitter{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-twitter-rgb),var(--tblr-bg-opacity))!important}.bg-linkedin{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-linkedin-rgb),var(--tblr-bg-opacity))!important}.bg-google{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-google-rgb),var(--tblr-bg-opacity))!important}.bg-youtube{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-youtube-rgb),var(--tblr-bg-opacity))!important}.bg-vimeo{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-vimeo-rgb),var(--tblr-bg-opacity))!important}.bg-dribbble{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-dribbble-rgb),var(--tblr-bg-opacity))!important}.bg-github{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-github-rgb),var(--tblr-bg-opacity))!important}.bg-instagram{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-instagram-rgb),var(--tblr-bg-opacity))!important}.bg-pinterest{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-pinterest-rgb),var(--tblr-bg-opacity))!important}.bg-vk{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-vk-rgb),var(--tblr-bg-opacity))!important}.bg-rss{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-rss-rgb),var(--tblr-bg-opacity))!important}.bg-flickr{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-flickr-rgb),var(--tblr-bg-opacity))!important}.bg-bitbucket{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-bg-opacity))!important}.bg-tabler{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-tabler-rgb),var(--tblr-bg-opacity))!important}.bg-black{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-black-rgb),var(--tblr-bg-opacity))!important}.bg-white{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-white-rgb),var(--tblr-bg-opacity))!important}.bg-body{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-body-bg-rgb),var(--tblr-bg-opacity))!important}.bg-transparent{--tblr-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--tblr-bg-opacity:0.1}.bg-opacity-25{--tblr-bg-opacity:0.25}.bg-opacity-50{--tblr-bg-opacity:0.5}.bg-opacity-75{--tblr-bg-opacity:0.75}.bg-opacity-100{--tblr-bg-opacity:1}.bg-gradient{background-image:var(--tblr-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--tblr-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--tblr-border-radius-sm)!important}.rounded-2{border-radius:var(--tblr-border-radius)!important}.rounded-3{border-radius:var(--tblr-border-radius-lg)!important}.rounded-4{border-radius:var(--tblr-border-radius-xl)!important}.rounded-5{border-radius:var(--tblr-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--tblr-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--tblr-border-radius)!important;border-top-right-radius:var(--tblr-border-radius)!important}.rounded-end{border-top-right-radius:var(--tblr-border-radius)!important;border-bottom-right-radius:var(--tblr-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--tblr-border-radius)!important;border-bottom-left-radius:var(--tblr-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--tblr-border-radius)!important;border-top-left-radius:var(--tblr-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.object-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-scale-down{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-none{-o-object-fit:none!important;object-fit:none!important}.tracking-tight{letter-spacing:-.05em!important}.tracking-normal{letter-spacing:0!important}.tracking-wide{letter-spacing:.05em!important}.cursor-auto{cursor:auto!important}.cursor-pointer{cursor:pointer!important}.cursor-move{cursor:move!important}.cursor-not-allowed{cursor:not-allowed!important}.cursor-zoom-in{cursor:zoom-in!important}.cursor-zoom-out{cursor:zoom-out!important}.cursor-default{cursor:default!important}.cursor-none{cursor:none!important}.cursor-help{cursor:help!important}.cursor-progress{cursor:progress!important}.cursor-wait{cursor:wait!important}.cursor-text{cursor:text!important}.cursor-v-text{cursor:vertical-text!important}.cursor-grab{cursor:-webkit-grab!important;cursor:grab!important}.cursor-grabbing{cursor:-webkit-grabbing!important;cursor:grabbing!important}.border-x{border-left:1px var(--tblr-border-style) rgba(97,104,118,.16)!important;border-right:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-x-wide{border-left:2px var(--tblr-border-style) rgba(97,104,118,.16)!important;border-right:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-x-0{border-left:0!important;border-right:0!important}.border-y{border-top:1px var(--tblr-border-style) rgba(97,104,118,.16)!important;border-bottom:1px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-y-wide{border-top:2px var(--tblr-border-style) rgba(97,104,118,.16)!important;border-bottom:2px var(--tblr-border-style) rgba(97,104,118,.16)!important}.border-y-0{border-top:0!important;border-bottom:0!important}.columns-2{-moz-columns:2!important;columns:2!important}.columns-3{-moz-columns:3!important;columns:3!important}.columns-4{-moz-columns:4!important;columns:4!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:2rem!important}.m-sm-5{margin:4rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:2rem!important;margin-left:2rem!important}.mx-sm-5{margin-right:4rem!important;margin-left:4rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-sm-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:2rem!important}.mt-sm-5{margin-top:4rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:2rem!important}.me-sm-5{margin-right:4rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:2rem!important}.mb-sm-5{margin-bottom:4rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:2rem!important}.ms-sm-5{margin-left:4rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:2rem!important}.p-sm-5{padding:4rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:2rem!important;padding-left:2rem!important}.px-sm-5{padding-right:4rem!important;padding-left:4rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-sm-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:2rem!important}.pt-sm-5{padding-top:4rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:2rem!important}.pe-sm-5{padding-right:4rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:2rem!important}.pb-sm-5{padding-bottom:4rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:2rem!important}.ps-sm-5{padding-left:4rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:2rem!important}.gap-sm-5{gap:4rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}.columns-sm-2{-moz-columns:2!important;columns:2!important}.columns-sm-3{-moz-columns:3!important;columns:3!important}.columns-sm-4{-moz-columns:4!important;columns:4!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:2rem!important}.m-md-5{margin:4rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:2rem!important;margin-left:2rem!important}.mx-md-5{margin-right:4rem!important;margin-left:4rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-md-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:2rem!important}.mt-md-5{margin-top:4rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:2rem!important}.me-md-5{margin-right:4rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:2rem!important}.mb-md-5{margin-bottom:4rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:2rem!important}.ms-md-5{margin-left:4rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:2rem!important}.p-md-5{padding:4rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:2rem!important;padding-left:2rem!important}.px-md-5{padding-right:4rem!important;padding-left:4rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-md-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:2rem!important}.pt-md-5{padding-top:4rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:2rem!important}.pe-md-5{padding-right:4rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:2rem!important}.pb-md-5{padding-bottom:4rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:2rem!important}.ps-md-5{padding-left:4rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:2rem!important}.gap-md-5{gap:4rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}.columns-md-2{-moz-columns:2!important;columns:2!important}.columns-md-3{-moz-columns:3!important;columns:3!important}.columns-md-4{-moz-columns:4!important;columns:4!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:2rem!important}.m-lg-5{margin:4rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:2rem!important;margin-left:2rem!important}.mx-lg-5{margin-right:4rem!important;margin-left:4rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-lg-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:2rem!important}.mt-lg-5{margin-top:4rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:2rem!important}.me-lg-5{margin-right:4rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:2rem!important}.mb-lg-5{margin-bottom:4rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:2rem!important}.ms-lg-5{margin-left:4rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:2rem!important}.p-lg-5{padding:4rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:2rem!important;padding-left:2rem!important}.px-lg-5{padding-right:4rem!important;padding-left:4rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-lg-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:2rem!important}.pt-lg-5{padding-top:4rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:2rem!important}.pe-lg-5{padding-right:4rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:2rem!important}.pb-lg-5{padding-bottom:4rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:2rem!important}.ps-lg-5{padding-left:4rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:2rem!important}.gap-lg-5{gap:4rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}.columns-lg-2{-moz-columns:2!important;columns:2!important}.columns-lg-3{-moz-columns:3!important;columns:3!important}.columns-lg-4{-moz-columns:4!important;columns:4!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:2rem!important}.m-xl-5{margin:4rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:2rem!important;margin-left:2rem!important}.mx-xl-5{margin-right:4rem!important;margin-left:4rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-xl-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:2rem!important}.mt-xl-5{margin-top:4rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:2rem!important}.me-xl-5{margin-right:4rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:2rem!important}.mb-xl-5{margin-bottom:4rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:2rem!important}.ms-xl-5{margin-left:4rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:2rem!important}.p-xl-5{padding:4rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:2rem!important;padding-left:2rem!important}.px-xl-5{padding-right:4rem!important;padding-left:4rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-xl-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:2rem!important}.pt-xl-5{padding-top:4rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:2rem!important}.pe-xl-5{padding-right:4rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:2rem!important}.pb-xl-5{padding-bottom:4rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:2rem!important}.ps-xl-5{padding-left:4rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:2rem!important}.gap-xl-5{gap:4rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}.columns-xl-2{-moz-columns:2!important;columns:2!important}.columns-xl-3{-moz-columns:3!important;columns:3!important}.columns-xl-4{-moz-columns:4!important;columns:4!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:2rem!important}.m-xxl-5{margin:4rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:2rem!important;margin-left:2rem!important}.mx-xxl-5{margin-right:4rem!important;margin-left:4rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:2rem!important;margin-bottom:2rem!important}.my-xxl-5{margin-top:4rem!important;margin-bottom:4rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:2rem!important}.mt-xxl-5{margin-top:4rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:2rem!important}.me-xxl-5{margin-right:4rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:2rem!important}.mb-xxl-5{margin-bottom:4rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:2rem!important}.ms-xxl-5{margin-left:4rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:2rem!important}.p-xxl-5{padding:4rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:2rem!important;padding-left:2rem!important}.px-xxl-5{padding-right:4rem!important;padding-left:4rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:2rem!important;padding-bottom:2rem!important}.py-xxl-5{padding-top:4rem!important;padding-bottom:4rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:2rem!important}.pt-xxl-5{padding-top:4rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:2rem!important}.pe-xxl-5{padding-right:4rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:2rem!important}.pb-xxl-5{padding-bottom:4rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:2rem!important}.ps-xxl-5{padding-left:4rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:2rem!important}.gap-xxl-5{gap:4rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}.columns-xxl-2{-moz-columns:2!important;columns:2!important}.columns-xxl-3{-moz-columns:3!important;columns:3!important}.columns-xxl-4{-moz-columns:4!important;columns:4!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}:host,:root{font-size:16px;height:100%;--tblr-primary:#206bc4;--tblr-primary-fg:var(--tblr-light);--tblr-primary-darken:#1d60b0;--tblr-primary-rgb:32,107,196;--tblr-secondary:#616876;--tblr-secondary-fg:var(--tblr-light);--tblr-secondary-darken:#575e6a;--tblr-secondary-rgb:97,104,118;--tblr-success:#2fb344;--tblr-success-fg:var(--tblr-light);--tblr-success-darken:#2aa13d;--tblr-success-rgb:47,179,68;--tblr-info:#4299e1;--tblr-info-fg:var(--tblr-light);--tblr-info-darken:#3b8acb;--tblr-info-rgb:66,153,225;--tblr-warning:#f76707;--tblr-warning-fg:var(--tblr-light);--tblr-warning-darken:#de5d06;--tblr-warning-rgb:247,103,7;--tblr-danger:#d63939;--tblr-danger-fg:var(--tblr-light);--tblr-danger-darken:#c13333;--tblr-danger-rgb:214,57,57;--tblr-light:#f8fafc;--tblr-light-fg:var(--tblr-dark);--tblr-light-darken:#dfe1e3;--tblr-light-rgb:248,250,252;--tblr-dark:#1d273b;--tblr-dark-fg:var(--tblr-light);--tblr-dark-darken:#1a2335;--tblr-dark-rgb:29,39,59;--tblr-muted:#616876;--tblr-muted-fg:var(--tblr-light);--tblr-muted-darken:#575e6a;--tblr-muted-rgb:97,104,118;--tblr-blue:#206bc4;--tblr-blue-fg:var(--tblr-light);--tblr-blue-darken:#1d60b0;--tblr-blue-rgb:32,107,196;--tblr-azure:#4299e1;--tblr-azure-fg:var(--tblr-light);--tblr-azure-darken:#3b8acb;--tblr-azure-rgb:66,153,225;--tblr-indigo:#4263eb;--tblr-indigo-fg:var(--tblr-light);--tblr-indigo-darken:#3b59d4;--tblr-indigo-rgb:66,99,235;--tblr-purple:#ae3ec9;--tblr-purple-fg:var(--tblr-light);--tblr-purple-darken:#9d38b5;--tblr-purple-rgb:174,62,201;--tblr-pink:#d6336c;--tblr-pink-fg:var(--tblr-light);--tblr-pink-darken:#c12e61;--tblr-pink-rgb:214,51,108;--tblr-red:#d63939;--tblr-red-fg:var(--tblr-light);--tblr-red-darken:#c13333;--tblr-red-rgb:214,57,57;--tblr-orange:#f76707;--tblr-orange-fg:var(--tblr-light);--tblr-orange-darken:#de5d06;--tblr-orange-rgb:247,103,7;--tblr-yellow:#f59f00;--tblr-yellow-fg:var(--tblr-light);--tblr-yellow-darken:#dd8f00;--tblr-yellow-rgb:245,159,0;--tblr-lime:#74b816;--tblr-lime-fg:var(--tblr-light);--tblr-lime-darken:#68a614;--tblr-lime-rgb:116,184,22;--tblr-green:#2fb344;--tblr-green-fg:var(--tblr-light);--tblr-green-darken:#2aa13d;--tblr-green-rgb:47,179,68;--tblr-teal:#0ca678;--tblr-teal-fg:var(--tblr-light);--tblr-teal-darken:#0b956c;--tblr-teal-rgb:12,166,120;--tblr-cyan:#17a2b8;--tblr-cyan-fg:var(--tblr-light);--tblr-cyan-darken:#1592a6;--tblr-cyan-rgb:23,162,184;--tblr-facebook:#1877F2;--tblr-facebook-fg:var(--tblr-light);--tblr-facebook-darken:#166bda;--tblr-facebook-rgb:24,119,242;--tblr-twitter:#1da1f2;--tblr-twitter-fg:var(--tblr-light);--tblr-twitter-darken:#1a91da;--tblr-twitter-rgb:29,161,242;--tblr-linkedin:#0a66c2;--tblr-linkedin-fg:var(--tblr-light);--tblr-linkedin-darken:#095caf;--tblr-linkedin-rgb:10,102,194;--tblr-google:#dc4e41;--tblr-google-fg:var(--tblr-light);--tblr-google-darken:#c6463b;--tblr-google-rgb:220,78,65;--tblr-youtube:#ff0000;--tblr-youtube-fg:var(--tblr-light);--tblr-youtube-darken:#e60000;--tblr-youtube-rgb:255,0,0;--tblr-vimeo:#1ab7ea;--tblr-vimeo-fg:var(--tblr-light);--tblr-vimeo-darken:#17a5d3;--tblr-vimeo-rgb:26,183,234;--tblr-dribbble:#ea4c89;--tblr-dribbble-fg:var(--tblr-light);--tblr-dribbble-darken:#d3447b;--tblr-dribbble-rgb:234,76,137;--tblr-github:#181717;--tblr-github-fg:var(--tblr-light);--tblr-github-darken:#161515;--tblr-github-rgb:24,23,23;--tblr-instagram:#e4405f;--tblr-instagram-fg:var(--tblr-light);--tblr-instagram-darken:#cd3a56;--tblr-instagram-rgb:228,64,95;--tblr-pinterest:#bd081c;--tblr-pinterest-fg:var(--tblr-light);--tblr-pinterest-darken:#aa0719;--tblr-pinterest-rgb:189,8,28;--tblr-vk:#6383a8;--tblr-vk-fg:var(--tblr-light);--tblr-vk-darken:#597697;--tblr-vk-rgb:99,131,168;--tblr-rss:#ffa500;--tblr-rss-fg:var(--tblr-light);--tblr-rss-darken:#e69500;--tblr-rss-rgb:255,165,0;--tblr-flickr:#0063dc;--tblr-flickr-fg:var(--tblr-light);--tblr-flickr-darken:#0059c6;--tblr-flickr-rgb:0,99,220;--tblr-bitbucket:#0052cc;--tblr-bitbucket-fg:var(--tblr-light);--tblr-bitbucket-darken:#004ab8;--tblr-bitbucket-rgb:0,82,204;--tblr-tabler:#206bc4;--tblr-tabler-fg:var(--tblr-light);--tblr-tabler-darken:#1d60b0;--tblr-tabler-rgb:32,107,196;--tblr-gray-50:#f8fafc;--tblr-gray-50-fg:var(--tblr-dark);--tblr-gray-50-darken:#dfe1e3;--tblr-gray-50-rgb:248,250,252;--tblr-gray-100:#f1f5f9;--tblr-gray-100-fg:var(--tblr-dark);--tblr-gray-100-darken:#d9dde0;--tblr-gray-100-rgb:241,245,249;--tblr-gray-200:#e2e8f0;--tblr-gray-200-fg:var(--tblr-dark);--tblr-gray-200-darken:#cbd1d8;--tblr-gray-200-rgb:226,232,240;--tblr-gray-300:#c8d3e1;--tblr-gray-300-fg:var(--tblr-dark);--tblr-gray-300-darken:#b4becb;--tblr-gray-300-rgb:200,211,225;--tblr-gray-400:#9ba9be;--tblr-gray-400-fg:var(--tblr-light);--tblr-gray-400-darken:#8c98ab;--tblr-gray-400-rgb:155,169,190;--tblr-gray-500:#6c7a91;--tblr-gray-500-fg:var(--tblr-light);--tblr-gray-500-darken:#616e83;--tblr-gray-500-rgb:108,122,145;--tblr-gray-600:#49566c;--tblr-gray-600-fg:var(--tblr-light);--tblr-gray-600-darken:#424d61;--tblr-gray-600-rgb:73,86,108;--tblr-gray-700:#313c52;--tblr-gray-700-fg:var(--tblr-light);--tblr-gray-700-darken:#2c364a;--tblr-gray-700-rgb:49,60,82;--tblr-gray-800:#1d273b;--tblr-gray-800-fg:var(--tblr-light);--tblr-gray-800-darken:#1a2335;--tblr-gray-800-rgb:29,39,59;--tblr-gray-900:#0f172a;--tblr-gray-900-fg:var(--tblr-light);--tblr-gray-900-darken:#0e1526;--tblr-gray-900-rgb:15,23,42;--tblr-bg-surface:var(--tblr-white);--tblr-bg-surface-secondary:var(--tblr-light);--tblr-bg-surface-dark:var(--tblr-dark);--tblr-bg-forms:var(--tblr-bg-surface);--tblr-border-color:#e6e7e9;--tblr-border-color-light:#f2f3f4;--tblr-border-color-active:#b3b7bd;--tblr-icon-color:var(--tblr-gray-500);--tblr-active-bg:rgba(var(--tblr-primary-rgb), 0.04);--tblr-disabled-bg:var(--tblr-gray-100);--tblr-disabled-color:var(--tblr-gray-300);--tblr-code-color:var(--tblr-gray-600);--tblr-code-bg:var(--tblr-gray-100);--tblr-dark-mode-border-color:#243049;--tblr-dark-mode-border-color-light:#243049;--tblr-dark-mode-border-color-active:#314264;--tblr-font-weight-light:300;--tblr-font-weight-normal:400;--tblr-font-weight-medium:500;--tblr-font-weight-bold:600;--tblr-font-weight-headings:var(--tblr-font-weight-medium);--tblr-font-size-h1:1.5rem;--tblr-font-size-h2:1.25rem;--tblr-font-size-h3:1rem;--tblr-font-size-h4:0.875rem;--tblr-font-size-h5:0.75rem;--tblr-font-size-h6:0.625rem;--tblr-line-height-h1:2rem;--tblr-line-height-h2:1.75rem;--tblr-line-height-h3:1.5rem;--tblr-line-height-h4:1.25rem;--tblr-line-height-h5:1rem;--tblr-line-height-h6:1rem;--tblr-shadow:rgba(var(--tblr-body-color-rgb), 0.04) 0 2px 4px 0;--tblr-shadow-transparent:0 0 0 0 transparent;--tblr-shadow-button:0 1px 0 rgba(var(--tblr-body-color-rgb), 0.04);--tblr-shadow-button-inset:inset 0 -1px 0 rgba(var(--tblr-body-color-rgb), 0.2);--tblr-shadow-card:0 0 4px rgba(var(--tblr-body-color-rgb), 0.04);--tblr-shadow-card-hover:rgba(var(--tblr-body-color-rgb), 0.16) 0 2px 16px 0}@keyframes pulse{from{opacity:1;transform:scale3d(.8,.8,.8)}50%{transform:scale3d(1,1,1);opacity:1}to{opacity:1;transform:scale3d(.8,.8,.8)}}@keyframes tada{0%{transform:scale3d(1,1,1)}10%,5%{transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-5deg)}15%,25%,35%,45%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,5deg)}20%,30%,40%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-5deg)}50%{transform:scale3d(1,1,1)}}@keyframes rotate-360{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes blink{from{opacity:0}50%{opacity:1}to{opacity:0}}body{overflow-y:scroll;letter-spacing:0;touch-action:manipulation;text-rendering:optimizeLegibility;font-feature-settings:"liga" 0;position:relative;min-height:100%;height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media print{body{background:0 0}}::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){::-webkit-scrollbar{-webkit-transition:none;transition:none}}::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-body-color-rgb),.16)}::-webkit-scrollbar-track{background:rgba(var(--tblr-body-color-rgb),.06)}:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-body-color-rgb),.32)}::-webkit-scrollbar-corner{background:0 0}.layout-fluid .container,.layout-fluid [class*=" container-"],.layout-fluid [class^=container-]{max-width:100%}.layout-boxed{--tblr-theme-boxed-border-radius:0;--tblr-theme-boxed-width:1320px}@media (min-width:768px){.layout-boxed{background:#1d273b linear-gradient(to right,rgba(255,255,255,.1),transparent) fixed;padding:1rem;--tblr-theme-boxed-border-radius:4px}}.layout-boxed .page{margin:0 auto;max-width:var(--tblr-theme-boxed-width);border-radius:var(--tblr-theme-boxed-border-radius);color:#1d273b}@media (min-width:768px){.layout-boxed .page{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);background:var(--tblr-body-bg)}}.layout-boxed .page>.navbar:first-child{border-top-left-radius:var(--tblr-theme-boxed-border-radius);border-top-right-radius:var(--tblr-theme-boxed-border-radius)}.navbar{--tblr-navbar-border-width:var(--tblr-border-width);--tblr-navbar-active-border-color:var(--tblr-primary);--tblr-navbar-active-bg:rgba(0, 0, 0, 0.06);--tblr-navbar-bg:transparent;align-items:stretch;min-height:3.5rem;box-shadow:inset 0 calc(-1 * var(--tblr-navbar-border-width)) 0 0 var(--tblr-navbar-border-color);background:var(--tblr-navbar-bg);--tblr-navbar-active-bg:rgba(0, 0, 0, 0.06);--tblr-navbar-bg:transparent;color:var(--tblr-body-color)}.navbar-collapse .navbar{flex-grow:1}.navbar.collapsing{min-height:0}.navbar .navbar-brand{color:var(--tblr-body-color)}.navbar .navbar-brand:focus,.navbar .navbar-brand:hover{color:var(--tblr-body-color);opacity:.8}.navbar .navbar-nav .nav-link{color:var(--tblr-body-color)}.navbar .navbar-nav .nav-link:focus,.navbar .navbar-nav .nav-link:hover{color:var(--tblr-body-color) color}.navbar .navbar-nav .nav-link.disabled{color:var(--tblr-disabled-color)}.navbar .navbar-nav .active>.nav-link,.navbar .navbar-nav .nav-link.active,.navbar .navbar-nav .nav-link.show,.navbar .navbar-nav .show>.nav-link{color:var(--tblr-body-color) color}.navbar .navbar-toggler{color:var(--tblr-body-color);border-color:transparent}.navbar .navbar-text{color:var(--tblr-body-color)}.navbar .navbar-text a,.navbar .navbar-text a:focus,.navbar .navbar-text a:hover{color:var(--tblr-body-color)}@media not print{.theme-dark .navbar{--tblr-navbar-border-color:#243049;--tblr-navbar-bg:#1d273b;--tblr-navbar-active-bg:rgba(255, 255, 255, 0.06);--tblr-navbar-bg:#1d273b;color:rgba(255,255,255,.7)}.theme-dark .navbar .navbar-brand{color:#fff}.theme-dark .navbar .navbar-brand:focus,.theme-dark .navbar .navbar-brand:hover{color:#fff;opacity:.8}.theme-dark .navbar .navbar-nav .nav-link{color:rgba(255,255,255,.7)}.theme-dark .navbar .navbar-nav .nav-link:focus,.theme-dark .navbar .navbar-nav .nav-link:hover{color:#fff}.theme-dark .navbar .navbar-nav .nav-link.disabled{color:var(--tblr-disabled-color)}.theme-dark .navbar .navbar-nav .active>.nav-link,.theme-dark .navbar .navbar-nav .nav-link.active,.theme-dark .navbar .navbar-nav .nav-link.show,.theme-dark .navbar .navbar-nav .show>.nav-link{color:#fff}.theme-dark .navbar .navbar-toggler{color:#fff;border-color:transparent}.theme-dark .navbar .navbar-text{color:rgba(255,255,255,.7)}.theme-dark .navbar .navbar-text a,.theme-dark .navbar .navbar-text a:focus,.theme-dark .navbar .navbar-text a:hover{color:rgba(255,255,255,.7)}.theme-dark .navbar::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){.theme-dark .navbar::-webkit-scrollbar{-webkit-transition:none;transition:none}}.theme-dark .navbar::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-body-color-rgb),.16)}.theme-dark .navbar::-webkit-scrollbar-track{background:rgba(var(--tblr-body-color-rgb),.06)}.theme-dark .navbar:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-body-color-rgb),.32)}.theme-dark .navbar::-webkit-scrollbar-corner{background:0 0}}@media not print{@media (prefers-color-scheme:dark){.theme-dark-auto .navbar{--tblr-navbar-border-color:#243049;--tblr-navbar-bg:#1d273b;--tblr-navbar-active-bg:rgba(255, 255, 255, 0.06);--tblr-navbar-bg:#1d273b;color:rgba(255,255,255,.7)}.theme-dark-auto .navbar .navbar-brand{color:#fff}.theme-dark-auto .navbar .navbar-brand:focus,.theme-dark-auto .navbar .navbar-brand:hover{color:#fff;opacity:.8}.theme-dark-auto .navbar .navbar-nav .nav-link{color:rgba(255,255,255,.7)}.theme-dark-auto .navbar .navbar-nav .nav-link:focus,.theme-dark-auto .navbar .navbar-nav .nav-link:hover{color:#fff}.theme-dark-auto .navbar .navbar-nav .nav-link.disabled{color:var(--tblr-disabled-color)}.theme-dark-auto .navbar .navbar-nav .active>.nav-link,.theme-dark-auto .navbar .navbar-nav .nav-link.active,.theme-dark-auto .navbar .navbar-nav .nav-link.show,.theme-dark-auto .navbar .navbar-nav .show>.nav-link{color:#fff}.theme-dark-auto .navbar .navbar-toggler{color:#fff;border-color:transparent}.theme-dark-auto .navbar .navbar-text{color:rgba(255,255,255,.7)}.theme-dark-auto .navbar .navbar-text a,.theme-dark-auto .navbar .navbar-text a:focus,.theme-dark-auto .navbar .navbar-text a:hover{color:rgba(255,255,255,.7)}.theme-dark-auto .navbar::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}}@media (prefers-color-scheme:dark) and (prefers-reduced-motion:reduce){.theme-dark-auto .navbar::-webkit-scrollbar{-webkit-transition:none;transition:none}}@media (prefers-color-scheme:dark){.theme-dark-auto .navbar::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-body-color-rgb),.16)}}@media (prefers-color-scheme:dark){.theme-dark-auto .navbar::-webkit-scrollbar-track{background:rgba(var(--tblr-body-color-rgb),.06)}}@media (prefers-color-scheme:dark){.theme-dark-auto .navbar:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-body-color-rgb),.32)}}@media (prefers-color-scheme:dark){.theme-dark-auto .navbar::-webkit-scrollbar-corner{background:0 0}}}.navbar .dropdown-menu{position:absolute;z-index:1030}.navbar .navbar-nav{min-height:3rem}.navbar .navbar-nav .nav-link{position:relative;min-width:2rem;min-height:2rem;justify-content:center;border-radius:var(--tblr-border-radius)}.navbar .navbar-nav .nav-link .badge{position:absolute;top:.375rem;right:.375rem;transform:translate(50%,-50%)}.navbar-nav{margin:0;padding:0}@media (max-width:575.98px){.navbar-expand-sm .navbar-collapse{flex-direction:column}.navbar-expand-sm .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-sm .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-sm .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand-sm .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-sm .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand-sm .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-sm .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:576px){.navbar-expand-sm .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-sm .navbar-light .nav-item.active,.navbar-expand-sm.navbar-light .nav-item.active{position:relative}.navbar-expand-sm .navbar-light .nav-item.active:after,.navbar-expand-sm.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-sm.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-sm.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-sm.navbar-vertical~.navbar,.navbar-expand-sm.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-sm.navbar-vertical.navbar-right~.navbar,.navbar-expand-sm.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:767.98px){.navbar-expand-md .navbar-collapse{flex-direction:column}.navbar-expand-md .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-md .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-md .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand-md .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-md .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand-md .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-md .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:768px){.navbar-expand-md .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-md .navbar-light .nav-item.active,.navbar-expand-md.navbar-light .nav-item.active{position:relative}.navbar-expand-md .navbar-light .nav-item.active:after,.navbar-expand-md.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-md.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-md.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-md.navbar-vertical~.navbar,.navbar-expand-md.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-md.navbar-vertical.navbar-right~.navbar,.navbar-expand-md.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:991.98px){.navbar-expand-lg .navbar-collapse{flex-direction:column}.navbar-expand-lg .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-lg .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-lg .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand-lg .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-lg .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand-lg .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-lg .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:992px){.navbar-expand-lg .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-lg .navbar-light .nav-item.active,.navbar-expand-lg.navbar-light .nav-item.active{position:relative}.navbar-expand-lg .navbar-light .nav-item.active:after,.navbar-expand-lg.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-lg.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-lg.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-lg.navbar-vertical~.navbar,.navbar-expand-lg.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-lg.navbar-vertical.navbar-right~.navbar,.navbar-expand-lg.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:1199.98px){.navbar-expand-xl .navbar-collapse{flex-direction:column}.navbar-expand-xl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-xl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-xl .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand-xl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-xl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand-xl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-xl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1200px){.navbar-expand-xl .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-xl .navbar-light .nav-item.active,.navbar-expand-xl.navbar-light .nav-item.active{position:relative}.navbar-expand-xl .navbar-light .nav-item.active:after,.navbar-expand-xl.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-xl.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xl.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xl.navbar-vertical~.navbar,.navbar-expand-xl.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-xl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xl.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:1399.98px){.navbar-expand-xxl .navbar-collapse{flex-direction:column}.navbar-expand-xxl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-xxl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-xxl .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand-xxl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-xxl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand-xxl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-xxl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1400px){.navbar-expand-xxl .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-xxl .navbar-light .nav-item.active,.navbar-expand-xxl.navbar-light .nav-item.active{position:relative}.navbar-expand-xxl .navbar-light .nav-item.active:after,.navbar-expand-xxl.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-xxl.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xxl.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xxl.navbar-vertical~.navbar,.navbar-expand-xxl.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-xxl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}.navbar-expand .navbar-collapse{flex-direction:column}.navbar-expand .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-expand .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-expand .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}.navbar-expand .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand .navbar-light .nav-item.active,.navbar-expand.navbar-light .nav-item.active{position:relative}.navbar-expand .navbar-light .nav-item.active:after,.navbar-expand.navbar-light .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand.navbar-vertical~.navbar,.navbar-expand.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand.navbar-vertical.navbar-right~.navbar,.navbar-expand.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}.navbar-brand{display:inline-flex;align-items:center;font-weight:var(--tblr-font-weight-bold);margin:0;line-height:1}.navbar-brand-image{height:2rem;width:auto}.navbar-toggler{border:0;width:2rem;height:2rem;position:relative;display:flex;align-items:center;justify-content:center}.navbar-toggler-icon{height:2px;width:1.25em;background:currentColor;border-radius:10px;transition:top .2s .2s,bottom .2s .2s,transform .2s,opacity 0s .2s;position:relative}@media (prefers-reduced-motion:reduce){.navbar-toggler-icon{transition:none}}.navbar-toggler-icon:after,.navbar-toggler-icon:before{content:"";display:block;height:inherit;width:inherit;border-radius:inherit;background:inherit;position:absolute;left:0;transition:inherit}@media (prefers-reduced-motion:reduce){.navbar-toggler-icon:after,.navbar-toggler-icon:before{transition:none}}.navbar-toggler-icon:before{top:-.45em}.navbar-toggler-icon:after{bottom:-.45em}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon{transform:rotate(45deg);transition:top .3s,bottom .3s,transform .3s .3s,opacity 0s .3s}@media (prefers-reduced-motion:reduce){.navbar-toggler[aria-expanded=true] .navbar-toggler-icon{transition:none}}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon:before{top:0;transform:rotate(-90deg)}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon:after{bottom:0;opacity:0}.navbar-light{--tblr-navbar-border-color:var(--tblr-border-color);--tblr-navbar-bg:var(--tblr-bg-surface)}.navbar-dark{--tblr-navbar-border-color:#243049;--tblr-navbar-bg:#1d273b;--tblr-navbar-active-bg:rgba(255, 255, 255, 0.06);--tblr-navbar-bg:#1d273b;color:rgba(255,255,255,.7)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff;opacity:.8}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.7)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:var(--tblr-disabled-color)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:#fff;border-color:transparent}.navbar-dark .navbar-text{color:rgba(255,255,255,.7)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:rgba(255,255,255,.7)}.navbar-dark::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){.navbar-dark::-webkit-scrollbar{-webkit-transition:none;transition:none}}.navbar-dark::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-body-color-rgb),.16)}.navbar-dark::-webkit-scrollbar-track{background:rgba(var(--tblr-body-color-rgb),.06)}.navbar-dark:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-body-color-rgb),.32)}.navbar-dark::-webkit-scrollbar-corner{background:0 0}.navbar-transparent{--tblr-navbar-border-color:transparent!important;background:0 0!important}.navbar-nav{align-items:stretch}.navbar-nav .nav-item{display:flex;flex-direction:column;justify-content:center}.navbar-side{margin:0;display:flex;flex-direction:row;align-items:center;justify-content:space-around}@media (min-width:576px){.navbar-vertical.navbar-expand-sm{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}}@media (min-width:576px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-sm{transition:none}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm.navbar-right{left:auto;right:0}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm .navbar-brand{padding:.75rem 0;justify-content:center}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm .navbar-collapse{align-items:stretch}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-sm .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm~.page{padding-left:15rem}.navbar-vertical.navbar-expand-sm~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm.navbar-right~.page{padding-left:0;padding-right:15rem}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-sm .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-sm .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:768px){.navbar-vertical.navbar-expand-md{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}}@media (min-width:768px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-md{transition:none}}@media (min-width:768px){.navbar-vertical.navbar-expand-md.navbar-right{left:auto;right:0}}@media (min-width:768px){.navbar-vertical.navbar-expand-md .navbar-brand{padding:.75rem 0;justify-content:center}}@media (min-width:768px){.navbar-vertical.navbar-expand-md .navbar-collapse{align-items:stretch}}@media (min-width:768px){.navbar-vertical.navbar-expand-md .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-md .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width:768px){.navbar-vertical.navbar-expand-md>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}}@media (min-width:768px){.navbar-vertical.navbar-expand-md~.page{padding-left:15rem}.navbar-vertical.navbar-expand-md~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.navbar-vertical.navbar-expand-md.navbar-right~.page{padding-left:0;padding-right:15rem}}@media (min-width:768px){.navbar-vertical.navbar-expand-md .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-md .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-md .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-md .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-md .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}}@media (min-width:992px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-lg{transition:none}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg.navbar-right{left:auto;right:0}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg .navbar-brand{padding:.75rem 0;justify-content:center}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg .navbar-collapse{align-items:stretch}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-lg .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg~.page{padding-left:15rem}.navbar-vertical.navbar-expand-lg~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg.navbar-right~.page{padding-left:0;padding-right:15rem}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-lg .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}}@media (min-width:1200px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-xl{transition:none}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl.navbar-right{left:auto;right:0}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl .navbar-brand{padding:.75rem 0;justify-content:center}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl .navbar-collapse{align-items:stretch}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-xl .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl~.page{padding-left:15rem}.navbar-vertical.navbar-expand-xl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl.navbar-right~.page{padding-left:0;padding-right:15rem}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-xl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-xl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}}@media (min-width:1400px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-xxl{transition:none}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl.navbar-right{left:auto;right:0}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl .navbar-brand{padding:.75rem 0;justify-content:center}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl .navbar-collapse{align-items:stretch}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-xxl .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl~.page{padding-left:15rem}.navbar-vertical.navbar-expand-xxl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl.navbar-right~.page{padding-left:0;padding-right:15rem}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-xxl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-xxl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}.navbar-vertical.navbar-expand{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-x:auto;padding:0}@media (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand{transition:none}}.navbar-vertical.navbar-expand.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand~.page{padding-left:15rem}.navbar-vertical.navbar-expand~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand .navbar-collapse .navbar-nav .nav-link{padding:.5rem .75rem;justify-content:flex-start}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:2.5rem;color:inherit}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:4rem}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:5.5rem}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}.navbar-overlap:after{content:"";height:9rem;position:absolute;top:100%;left:0;right:0;background:inherit;z-index:-1;box-shadow:inherit}.page{display:flex;flex-direction:column;position:relative;min-height:100%}.page-center{justify-content:center}.page-wrapper{flex:1;display:flex;flex-direction:column}@media print{.page-wrapper{margin:0!important}}.page-wrapper-full .page-body:first-child{margin:0;border-top:0}.page-body{margin-top:1.25rem;margin-bottom:1.25rem}.page-body-card{background:var(--tblr-bg-surface);border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);padding:1.25rem 0;margin-bottom:0;flex:1}.page-body~.page-body-card{margin-top:0}.page-cover{background:no-repeat center/cover;min-height:9rem}@media (min-width:768px){.page-cover{min-height:12rem}}@media (min-width:992px){.page-cover{min-height:15rem}}.page-cover-overlay{position:relative}.page-cover-overlay:after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,.6) 100%)}.page-header{display:flex;flex-wrap:wrap;min-height:2.25rem;flex-direction:column;justify-content:center}.page-wrapper .page-header{margin:1.25rem 0 0}.page-header-border{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);padding:1.25rem 0;margin:0!important;background-color:var(--tblr-bg-surface)}.page-pretitle{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted)}.page-title{margin:0;font-size:1.25rem;line-height:1.75rem;font-weight:var(--tblr-font-weight-bold);color:inherit;display:flex;align-items:center}.page-title svg{width:1.5rem;height:1.5rem;margin-right:.25rem}.page-title-lg{font-size:1.5rem;line-height:2rem}.page-subtitle{margin-top:.25rem;color:var(--tblr-muted)}.page-tabs{margin-top:.5rem;position:relative}.page-header-tabs .nav-bordered{border:0}.page-header-tabs+.page-body-card{margin-top:0}.footer{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);background-color:#fff;padding:2rem 0;color:var(--tblr-muted);margin-top:auto}.footer-transparent{background-color:transparent;border-top:0}/*! + * Tabler (v0.9.0): _dark.scss + * Copyright 2018-2021 The Tabler Authors + * Copyright 2018-2021 codecalm + * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) + */body:not(.theme-dark) .hide-theme-light{display:none!important}@media not print{.theme-dark{--tblr-body-color:#f8fafc;--tblr-body-color-rgb:248,250,252;--tblr-muted:rgba(153, 159, 164, 1);--tblr-body-bg:#1a2234;--tblr-body-bg-rgb:26,34,52;--tblr-bg-forms:#1a2234;--tblr-bg-surface:#1d273b;--tblr-bg-surface-dark:#1a2234;--tblr-bg-surface-secondary:#1a2234;--tblr-link-color:#307fdd;--tblr-link-hover-color:#206bc4;--tblr-active-bg:#202c42;--tblr-disabled-color:var(--tblr-gray-700);--tblr-card-bg:#1a2234;--tblr-card-bg-hover:#1a2234;--tblr-card-bg-rgb:26,34,52;--tblr-card-color:#f8fafc;--tblr-border-color:var(--tblr-dark-mode-border-color);--tblr-border-color-light:var(--tblr-dark-mode-border-color-light);--tblr-border-color-active:var(--tblr-dark-mode-border-color-active);--tblr-btn-color:#1a2234;--tblr-code-color:var(--tblr-body-color);--tblr-code-bg:#243049;color:#f8fafc;background-color:#1a2234}.theme-dark .page{color:#f8fafc}.theme-dark .hide-theme-dark{display:none!important}.theme-dark .text-body{color:#f8fafc!important}.theme-dark .alert:not(.alert-important),.theme-dark .card,.theme-dark .card-footer,.theme-dark .card-stacked::after,.theme-dark .dropdown-menu,.theme-dark .footer:not(.footer-transparent),.theme-dark .modal-content,.theme-dark .modal-header,.theme-dark .toast,.theme-dark .toast-header{background-color:var(--tblr-bg-surface);color:inherit}.theme-dark .modal{--tblr-modal-border-color:var(--tblr-border-color)}.theme-dark .bg-light{background-color:#1a2234!important}.theme-dark .card-tabs .nav-tabs .nav-link{background-color:#1a2234;color:inherit}.theme-dark .card-tabs .nav-tabs .nav-link.active{background-color:#1d273b;color:inherit}.theme-dark .form-check-input:not(:checked),.theme-dark .form-control,.theme-dark .form-file-text,.theme-dark .form-select,.theme-dark .form-selectgroup-check{background-color:#1a2234;color:#f8fafc;border-color:#243049}.theme-dark .form-control-plaintext{color:#f8fafc}.theme-dark .input-group-flat .input-group-text{background-color:#1a2234}.theme-dark .input-group-text{border-color:#243049}.theme-dark .highlight{background-color:#1a2234}.theme-dark .avatar{--tblr-avatar-bg:#202c42}.theme-dark .accordion-button,.theme-dark .markdown,.theme-dark .markdown>*{color:inherit}.theme-dark .accordion-button:after,.theme-dark .btn-close{filter:invert(1) grayscale(100%) brightness(200%)}.theme-dark .apexcharts-text{fill:#f8fafc}.theme-dark .apexcharts-gridline{stroke:var(--tblr-border-color)}.theme-dark .apexcharts-legend-text{color:inherit!important}.theme-dark .navbar-brand-autodark{filter:brightness(0) invert(1)}.theme-dark .input-group-text,.theme-dark .markdown>table thead th,.theme-dark .table thead th{background:0 0}.theme-dark .list-group-header{background:#1a2234}.theme-dark .list-group-item:not(.disabled):not(:disabled){color:#f8fafc}.theme-dark .list-group-item.disabled,.theme-dark .list-group-item:disabled{color:#49566c}.theme-dark .apexcharts-radialbar-area{stroke:#243049}.theme-dark .form-control.is-invalid,.theme-dark .was-validated .form-control:invalid{border-color:var(--tblr-danger)}.theme-dark .form-control.is-valid,.theme-dark .was-validated .form-control:valid{border-color:var(--tblr-success)}}@media not print{@media (prefers-color-scheme:dark){.theme-dark-auto{--tblr-body-color:#f8fafc;--tblr-body-color-rgb:248,250,252;--tblr-muted:rgba(153, 159, 164, 1);--tblr-body-bg:#1a2234;--tblr-body-bg-rgb:26,34,52;--tblr-bg-forms:#1a2234;--tblr-bg-surface:#1d273b;--tblr-bg-surface-dark:#1a2234;--tblr-bg-surface-secondary:#1a2234;--tblr-link-color:#307fdd;--tblr-link-hover-color:#206bc4;--tblr-active-bg:#202c42;--tblr-disabled-color:var(--tblr-gray-700);--tblr-card-bg:#1a2234;--tblr-card-bg-hover:#1a2234;--tblr-card-bg-rgb:26,34,52;--tblr-card-color:#f8fafc;--tblr-border-color:var(--tblr-dark-mode-border-color);--tblr-border-color-light:var(--tblr-dark-mode-border-color-light);--tblr-border-color-active:var(--tblr-dark-mode-border-color-active);--tblr-btn-color:#1a2234;--tblr-code-color:var(--tblr-body-color);--tblr-code-bg:#243049;color:#f8fafc;background-color:#1a2234}.theme-dark-auto .page{color:#f8fafc}.theme-dark-auto .hide-theme-dark{display:none!important}.theme-dark-auto .text-body{color:#f8fafc!important}.theme-dark-auto .alert:not(.alert-important),.theme-dark-auto .card,.theme-dark-auto .card-footer,.theme-dark-auto .card-stacked::after,.theme-dark-auto .dropdown-menu,.theme-dark-auto .footer:not(.footer-transparent),.theme-dark-auto .modal-content,.theme-dark-auto .modal-header,.theme-dark-auto .toast,.theme-dark-auto .toast-header{background-color:var(--tblr-bg-surface);color:inherit}.theme-dark-auto .modal{--tblr-modal-border-color:var(--tblr-border-color)}.theme-dark-auto .bg-light{background-color:#1a2234!important}.theme-dark-auto .card-tabs .nav-tabs .nav-link{background-color:#1a2234;color:inherit}.theme-dark-auto .card-tabs .nav-tabs .nav-link.active{background-color:#1d273b;color:inherit}.theme-dark-auto .form-check-input:not(:checked),.theme-dark-auto .form-control,.theme-dark-auto .form-file-text,.theme-dark-auto .form-select,.theme-dark-auto .form-selectgroup-check{background-color:#1a2234;color:#f8fafc;border-color:#243049}.theme-dark-auto .form-control-plaintext{color:#f8fafc}.theme-dark-auto .input-group-flat .input-group-text{background-color:#1a2234}.theme-dark-auto .input-group-text{border-color:#243049}.theme-dark-auto .highlight{background-color:#1a2234}.theme-dark-auto .avatar{--tblr-avatar-bg:#202c42}.theme-dark-auto .accordion-button,.theme-dark-auto .markdown,.theme-dark-auto .markdown>*{color:inherit}.theme-dark-auto .accordion-button:after,.theme-dark-auto .btn-close{filter:invert(1) grayscale(100%) brightness(200%)}.theme-dark-auto .apexcharts-text{fill:#f8fafc}.theme-dark-auto .apexcharts-gridline{stroke:var(--tblr-border-color)}.theme-dark-auto .apexcharts-legend-text{color:inherit!important}.theme-dark-auto .navbar-brand-autodark{filter:brightness(0) invert(1)}.theme-dark-auto .input-group-text,.theme-dark-auto .markdown>table thead th,.theme-dark-auto .table thead th{background:0 0}.theme-dark-auto .list-group-header{background:#1a2234}.theme-dark-auto .list-group-item:not(.disabled):not(:disabled){color:#f8fafc}.theme-dark-auto .list-group-item.disabled,.theme-dark-auto .list-group-item:disabled{color:#49566c}.theme-dark-auto .apexcharts-radialbar-area{stroke:#243049}.theme-dark-auto .form-control.is-invalid,.theme-dark-auto .was-validated .form-control:invalid{border-color:var(--tblr-danger)}.theme-dark-auto .form-control.is-valid,.theme-dark-auto .was-validated .form-control:valid{border-color:var(--tblr-success)}}}.accordion{--tblr-accordion-color:var(--tblr-body-color)}.accordion-button:focus:not(:focus-visible){outline:0;box-shadow:none}.accordion-button:after{opacity:.7}.accordion-button:not(.collapsed){font-weight:var(--tblr-font-weight-bold);border-bottom-color:transparent;box-shadow:none}.accordion-button:not(.collapsed):after{opacity:1}.alert{--tblr-alert-color:var(--tblr-muted);background:#fff;border:1px var(--tblr-border-style) var(--tblr-border-color-translucent);border-left:.25rem var(--tblr-border-style) var(--tblr-alert-color);box-shadow:rgba(29,39,59,.04) 0 2px 4px 0}.alert>:last-child{margin-bottom:0}.alert-important{border-color:transparent;background:var(--tblr-alert-color);color:#fff}.alert-important .alert-icon,.alert-important .alert-link{color:inherit}.alert-important .alert-link:hover{color:inherit}.alert-link,.alert-link:hover{color:var(--tblr-alert-color)}.alert-primary{--tblr-alert-color:var(--tblr-primary)}.alert-secondary{--tblr-alert-color:var(--tblr-secondary)}.alert-success{--tblr-alert-color:var(--tblr-success)}.alert-info{--tblr-alert-color:var(--tblr-info)}.alert-warning{--tblr-alert-color:var(--tblr-warning)}.alert-danger{--tblr-alert-color:var(--tblr-danger)}.alert-light{--tblr-alert-color:var(--tblr-light)}.alert-dark{--tblr-alert-color:var(--tblr-dark)}.alert-muted{--tblr-alert-color:var(--tblr-muted)}.alert-blue{--tblr-alert-color:var(--tblr-blue)}.alert-azure{--tblr-alert-color:var(--tblr-azure)}.alert-indigo{--tblr-alert-color:var(--tblr-indigo)}.alert-purple{--tblr-alert-color:var(--tblr-purple)}.alert-pink{--tblr-alert-color:var(--tblr-pink)}.alert-red{--tblr-alert-color:var(--tblr-red)}.alert-orange{--tblr-alert-color:var(--tblr-orange)}.alert-yellow{--tblr-alert-color:var(--tblr-yellow)}.alert-lime{--tblr-alert-color:var(--tblr-lime)}.alert-green{--tblr-alert-color:var(--tblr-green)}.alert-teal{--tblr-alert-color:var(--tblr-teal)}.alert-cyan{--tblr-alert-color:var(--tblr-cyan)}.alert-facebook{--tblr-alert-color:var(--tblr-facebook)}.alert-twitter{--tblr-alert-color:var(--tblr-twitter)}.alert-linkedin{--tblr-alert-color:var(--tblr-linkedin)}.alert-google{--tblr-alert-color:var(--tblr-google)}.alert-youtube{--tblr-alert-color:var(--tblr-youtube)}.alert-vimeo{--tblr-alert-color:var(--tblr-vimeo)}.alert-dribbble{--tblr-alert-color:var(--tblr-dribbble)}.alert-github{--tblr-alert-color:var(--tblr-github)}.alert-instagram{--tblr-alert-color:var(--tblr-instagram)}.alert-pinterest{--tblr-alert-color:var(--tblr-pinterest)}.alert-vk{--tblr-alert-color:var(--tblr-vk)}.alert-rss{--tblr-alert-color:var(--tblr-rss)}.alert-flickr{--tblr-alert-color:var(--tblr-flickr)}.alert-bitbucket{--tblr-alert-color:var(--tblr-bitbucket)}.alert-tabler{--tblr-alert-color:var(--tblr-tabler)}.alert-icon{color:var(--tblr-alert-color);width:1.5rem!important;height:1.5rem!important;margin:-.125rem 1rem -.125rem 0}.alert-title{font-size:.875rem;line-height:1.25rem;font-weight:var(--tblr-font-weight-bold);margin-bottom:.25rem;color:var(--tblr-alert-color)}.avatar{--tblr-avatar-size:2.5rem;--tblr-avatar-bg:var(--tblr-gray-100);position:relative;width:var(--tblr-avatar-size);height:var(--tblr-avatar-size);font-size:calc(var(--tblr-avatar-size)/ 2.8571428572);font-weight:var(--tblr-font-weight-medium);display:inline-flex;align-items:center;justify-content:center;color:var(--tblr-muted);text-align:center;text-transform:uppercase;vertical-align:bottom;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:var(--tblr-avatar-bg) no-repeat center/cover;border-radius:var(--tblr-border-radius)}.avatar svg{width:calc(var(--tblr-avatar-size)/ 1.6666666667);height:calc(var(--tblr-avatar-size)/ 1.6666666667)}.avatar .badge{position:absolute;right:0;bottom:0;border-radius:100rem;box-shadow:0 0 0 2px var(--tblr-bg-surface)}a.avatar{cursor:pointer}.avatar-rounded{border-radius:100rem}.avatar-xs{--tblr-avatar-size:1.5rem}.avatar-xs .badge:empty{width:.375rem;height:.375rem}.avatar-sm{--tblr-avatar-size:2rem}.avatar-sm .badge:empty{width:.5rem;height:.5rem}.avatar-md{--tblr-avatar-size:4rem}.avatar-md .badge:empty{width:1rem;height:1rem}.avatar-lg{--tblr-avatar-size:5rem}.avatar-lg .badge:empty{width:1.25rem;height:1.25rem}.avatar-xl{--tblr-avatar-size:7rem}.avatar-xl .badge:empty{width:1.75rem;height:1.75rem}.avatar-2xl{--tblr-avatar-size:11rem}.avatar-2xl .badge:empty{width:2.75rem;height:2.75rem}.avatar-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.avatar-list a.avatar:hover{z-index:1}.avatar-list-stacked .avatar{margin-right:-.5rem!important;box-shadow:0 0 0 2px 0 0 0 2px var(--tblr-card-cap-bg,var(--tblr-card-bg,var(--tblr-bg-surface)))}.avatar-upload{width:4rem;height:4rem;border:1px dashed var(--tblr-border-color);background:var(--tblr-bg-forms);flex-direction:column;transition:color .3s,background-color .3s}@media (prefers-reduced-motion:reduce){.avatar-upload{transition:none}}.avatar-upload svg{width:1.5rem;height:1.5rem;stroke-width:1}.avatar-upload:hover{border-color:var(--tblr-primary);color:var(--tblr-primary);text-decoration:none}.avatar-upload-text{font-size:.625rem;line-height:1;margin-top:.25rem}.page-cover~* .page-avatar{margin-top:calc(calc(-1 * calc(var(--tblr-avatar-size) * .5)) - 1.25rem);box-shadow:0 0 0 .25rem #f1f5f9}.badge{justify-content:center;align-items:center;background:#6c7a91;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) transparent;min-width:1.35714285em;font-weight:var(--tblr-font-weight-medium);letter-spacing:.04em;vertical-align:bottom}a.badge{color:var(--tblr-bg-surface)}.badge:empty{display:inline-block;width:.5rem;height:.5rem;min-width:0;min-height:auto;padding:0;border-radius:100rem;vertical-align:baseline}.badge .avatar{box-sizing:content-box;width:1.25rem;height:1.25rem;margin:0 .5rem 0 -.5rem}.badge .icon{width:1em;height:1em;font-size:1rem;stroke-width:2}.badge-outline{background-color:transparent;border:var(--tblr-border-width) var(--tblr-border-style) currentColor}.badge-pill{border-radius:100rem}.badges-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.badge-notification{position:absolute!important;top:0!important;right:0!important;transform:translate(50%,-50%);z-index:1}.badge-blink{animation:blink 2s infinite}.breadcrumb{--tblr-breadcrumb-item-active-font-weight:var(--tblr-font-weight-bold);--tblr-breadcrumb-item-disabled-color:var(--tblr-disabled-color);--tblr-breadcrumb-link-color:var(--tblr-link-color);padding:0;margin:0;background:0 0}.breadcrumb a{color:var(--tblr-breadcrumb-link-color)}.breadcrumb a:hover{text-decoration:underline}.breadcrumb-muted{--tblr-breadcrumb-link-color:var(--tblr-muted)}.breadcrumb-item.active{font-weight:var(--tblr-breadcrumb-item-active-font-weight)}.breadcrumb-item.active a{color:inherit;pointer-events:none}.breadcrumb-item.disabled{color:var(--tblr-breadcrumb-item-disabled-color)}.breadcrumb-item.disabled:before{color:inherit}.breadcrumb-item.disabled a{color:inherit;pointer-events:none}.breadcrumb-dots{--tblr-breadcrumb-divider:"·"}.breadcrumb-arrows{--tblr-breadcrumb-divider:"›"}.breadcrumb-bullets{--tblr-breadcrumb-divider:"•"}.btn{--tblr-btn-icon-size:1.25rem;--tblr-btn-bg:var(--tblr-bg-surface);--tblr-btn-color:var(--tblr-body-color);--tblr-btn-border-color:var(--tblr-border-color);--tblr-btn-hover-bg:var(--tblr-btn-bg);--tblr-btn-hover-border-color:var(--tblr-border-color-active);--tblr-btn-box-shadow:var(--tblr-shadow-button);--tblr-btn-active-color:var(--tblr-primary);--tblr-btn-active-bg:rgba(var(--tblr-primary-rgb), 0.04);--tblr-btn-active-border-color:var(--tblr-primary);display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;box-shadow:var(--tblr-btn-box-shadow)}.btn .icon{width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);min-width:var(--tblr-btn-icon-size);margin:0 calc(var(--tblr-btn-padding-x)/ 2) 0 calc(var(--tblr-btn-padding-x)/ -4);vertical-align:bottom;color:inherit}.btn .avatar{width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);margin:0 calc(var(--tblr-btn-padding-x)/ 2) 0 calc(var(--tblr-btn-padding-x)/ -4)}.btn .icon-right{margin:0 calc(var(--tblr-btn-padding-x)/ -4) 0 calc(var(--tblr-btn-padding-x)/ 2)}.btn .badge{top:auto}.btn-check+.btn:hover{color:var(--tblr-btn-hover-color);background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color)}.btn-link{color:var(--tblr-primary);background-color:transparent;border-color:transparent;box-shadow:none}.btn-link .icon{color:inherit}.btn-link:hover{color:var(--tblr-primary-darken);border-color:transparent}.btn-primary{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-primary-fg);--tblr-btn-bg:var(--tblr-primary);--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-bg:rgba(var(--tblr-primary-rgb), .8);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:rgba(var(--tblr-primary-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-primary);--tblr-btn-disabled-color:var(--tblr-primary-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-primary{--tblr-btn-color:var(--tblr-primary);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-primary);--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-primary);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:var(--tblr-primary);--tblr-btn-disabled-color:var(--tblr-primary);--tblr-btn-disabled-border-color:var(--tblr-primary)}.btn-secondary{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-secondary-fg);--tblr-btn-bg:var(--tblr-secondary);--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-bg:rgba(var(--tblr-secondary-rgb), .8);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:rgba(var(--tblr-secondary-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-secondary);--tblr-btn-disabled-color:var(--tblr-secondary-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-secondary{--tblr-btn-color:var(--tblr-secondary);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-secondary);--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-secondary);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:var(--tblr-secondary);--tblr-btn-disabled-color:var(--tblr-secondary);--tblr-btn-disabled-border-color:var(--tblr-secondary)}.btn-success{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-success-fg);--tblr-btn-bg:var(--tblr-success);--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-bg:rgba(var(--tblr-success-rgb), .8);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:rgba(var(--tblr-success-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-success);--tblr-btn-disabled-color:var(--tblr-success-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-success{--tblr-btn-color:var(--tblr-success);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-success);--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-success);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:var(--tblr-success);--tblr-btn-disabled-color:var(--tblr-success);--tblr-btn-disabled-border-color:var(--tblr-success)}.btn-info{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-info-fg);--tblr-btn-bg:var(--tblr-info);--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-bg:rgba(var(--tblr-info-rgb), .8);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:rgba(var(--tblr-info-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-info);--tblr-btn-disabled-color:var(--tblr-info-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-info{--tblr-btn-color:var(--tblr-info);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-info);--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-info);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:var(--tblr-info);--tblr-btn-disabled-color:var(--tblr-info);--tblr-btn-disabled-border-color:var(--tblr-info)}.btn-warning{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-warning-fg);--tblr-btn-bg:var(--tblr-warning);--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-bg:rgba(var(--tblr-warning-rgb), .8);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:rgba(var(--tblr-warning-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-warning);--tblr-btn-disabled-color:var(--tblr-warning-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-warning{--tblr-btn-color:var(--tblr-warning);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-warning);--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-warning);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:var(--tblr-warning);--tblr-btn-disabled-color:var(--tblr-warning);--tblr-btn-disabled-border-color:var(--tblr-warning)}.btn-danger{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-danger-fg);--tblr-btn-bg:var(--tblr-danger);--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-bg:rgba(var(--tblr-danger-rgb), .8);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:rgba(var(--tblr-danger-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-danger);--tblr-btn-disabled-color:var(--tblr-danger-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-danger{--tblr-btn-color:var(--tblr-danger);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-danger);--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-danger);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:var(--tblr-danger);--tblr-btn-disabled-color:var(--tblr-danger);--tblr-btn-disabled-border-color:var(--tblr-danger)}.btn-light{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-light-fg);--tblr-btn-bg:var(--tblr-light);--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-bg:rgba(var(--tblr-light-rgb), .8);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:rgba(var(--tblr-light-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-light);--tblr-btn-disabled-color:var(--tblr-light-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-light{--tblr-btn-color:var(--tblr-light);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-light);--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-light);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:var(--tblr-light);--tblr-btn-disabled-color:var(--tblr-light);--tblr-btn-disabled-border-color:var(--tblr-light)}.btn-dark{--tblr-btn-border-color:var(--tblr-dark-mode-border-color);--tblr-btn-hover-border-color:var(--tblr-dark-mode-border-color-active);--tblr-btn-active-border-color:var(--tblr-dark-mode-border-color-active);--tblr-btn-color:var(--tblr-dark-fg);--tblr-btn-bg:var(--tblr-dark);--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-bg:rgba(var(--tblr-dark-rgb), .8);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:rgba(var(--tblr-dark-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-dark);--tblr-btn-disabled-color:var(--tblr-dark-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-dark{--tblr-btn-color:var(--tblr-dark);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-dark);--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-dark);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:var(--tblr-dark);--tblr-btn-disabled-color:var(--tblr-dark);--tblr-btn-disabled-border-color:var(--tblr-dark)}.btn-muted{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-muted-fg);--tblr-btn-bg:var(--tblr-muted);--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-bg:rgba(var(--tblr-muted-rgb), .8);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:rgba(var(--tblr-muted-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-muted);--tblr-btn-disabled-color:var(--tblr-muted-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-muted{--tblr-btn-color:var(--tblr-muted);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-muted);--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-muted);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:var(--tblr-muted);--tblr-btn-disabled-color:var(--tblr-muted);--tblr-btn-disabled-border-color:var(--tblr-muted)}.btn-blue{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-blue-fg);--tblr-btn-bg:var(--tblr-blue);--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-bg:rgba(var(--tblr-blue-rgb), .8);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:rgba(var(--tblr-blue-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-blue);--tblr-btn-disabled-color:var(--tblr-blue-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-blue{--tblr-btn-color:var(--tblr-blue);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-blue);--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-blue);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:var(--tblr-blue);--tblr-btn-disabled-color:var(--tblr-blue);--tblr-btn-disabled-border-color:var(--tblr-blue)}.btn-azure{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-azure-fg);--tblr-btn-bg:var(--tblr-azure);--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-bg:rgba(var(--tblr-azure-rgb), .8);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:rgba(var(--tblr-azure-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-azure);--tblr-btn-disabled-color:var(--tblr-azure-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-azure{--tblr-btn-color:var(--tblr-azure);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-azure);--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-azure);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:var(--tblr-azure);--tblr-btn-disabled-color:var(--tblr-azure);--tblr-btn-disabled-border-color:var(--tblr-azure)}.btn-indigo{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-indigo-fg);--tblr-btn-bg:var(--tblr-indigo);--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-bg:rgba(var(--tblr-indigo-rgb), .8);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:rgba(var(--tblr-indigo-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-indigo);--tblr-btn-disabled-color:var(--tblr-indigo-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-indigo{--tblr-btn-color:var(--tblr-indigo);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-indigo);--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-indigo);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:var(--tblr-indigo);--tblr-btn-disabled-color:var(--tblr-indigo);--tblr-btn-disabled-border-color:var(--tblr-indigo)}.btn-purple{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-purple-fg);--tblr-btn-bg:var(--tblr-purple);--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-bg:rgba(var(--tblr-purple-rgb), .8);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:rgba(var(--tblr-purple-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-purple);--tblr-btn-disabled-color:var(--tblr-purple-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-purple{--tblr-btn-color:var(--tblr-purple);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-purple);--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-purple);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:var(--tblr-purple);--tblr-btn-disabled-color:var(--tblr-purple);--tblr-btn-disabled-border-color:var(--tblr-purple)}.btn-pink{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-pink-fg);--tblr-btn-bg:var(--tblr-pink);--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-bg:rgba(var(--tblr-pink-rgb), .8);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:rgba(var(--tblr-pink-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-pink);--tblr-btn-disabled-color:var(--tblr-pink-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-pink{--tblr-btn-color:var(--tblr-pink);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-pink);--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-pink);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:var(--tblr-pink);--tblr-btn-disabled-color:var(--tblr-pink);--tblr-btn-disabled-border-color:var(--tblr-pink)}.btn-red{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-red-fg);--tblr-btn-bg:var(--tblr-red);--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-bg:rgba(var(--tblr-red-rgb), .8);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:rgba(var(--tblr-red-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-red);--tblr-btn-disabled-color:var(--tblr-red-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-red{--tblr-btn-color:var(--tblr-red);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-red);--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-red);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:var(--tblr-red);--tblr-btn-disabled-color:var(--tblr-red);--tblr-btn-disabled-border-color:var(--tblr-red)}.btn-orange{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-orange-fg);--tblr-btn-bg:var(--tblr-orange);--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-bg:rgba(var(--tblr-orange-rgb), .8);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:rgba(var(--tblr-orange-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-orange);--tblr-btn-disabled-color:var(--tblr-orange-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-orange{--tblr-btn-color:var(--tblr-orange);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-orange);--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-orange);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:var(--tblr-orange);--tblr-btn-disabled-color:var(--tblr-orange);--tblr-btn-disabled-border-color:var(--tblr-orange)}.btn-yellow{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-yellow-fg);--tblr-btn-bg:var(--tblr-yellow);--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-bg:rgba(var(--tblr-yellow-rgb), .8);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:rgba(var(--tblr-yellow-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-yellow);--tblr-btn-disabled-color:var(--tblr-yellow-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-yellow{--tblr-btn-color:var(--tblr-yellow);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-yellow);--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-yellow);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:var(--tblr-yellow);--tblr-btn-disabled-color:var(--tblr-yellow);--tblr-btn-disabled-border-color:var(--tblr-yellow)}.btn-lime{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-lime-fg);--tblr-btn-bg:var(--tblr-lime);--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-bg:rgba(var(--tblr-lime-rgb), .8);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:rgba(var(--tblr-lime-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-lime);--tblr-btn-disabled-color:var(--tblr-lime-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-lime{--tblr-btn-color:var(--tblr-lime);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-lime);--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-lime);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:var(--tblr-lime);--tblr-btn-disabled-color:var(--tblr-lime);--tblr-btn-disabled-border-color:var(--tblr-lime)}.btn-green{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-green-fg);--tblr-btn-bg:var(--tblr-green);--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-bg:rgba(var(--tblr-green-rgb), .8);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:rgba(var(--tblr-green-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-green);--tblr-btn-disabled-color:var(--tblr-green-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-green{--tblr-btn-color:var(--tblr-green);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-green);--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-green);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:var(--tblr-green);--tblr-btn-disabled-color:var(--tblr-green);--tblr-btn-disabled-border-color:var(--tblr-green)}.btn-teal{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-teal-fg);--tblr-btn-bg:var(--tblr-teal);--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-bg:rgba(var(--tblr-teal-rgb), .8);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:rgba(var(--tblr-teal-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-teal);--tblr-btn-disabled-color:var(--tblr-teal-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-teal{--tblr-btn-color:var(--tblr-teal);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-teal);--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-teal);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:var(--tblr-teal);--tblr-btn-disabled-color:var(--tblr-teal);--tblr-btn-disabled-border-color:var(--tblr-teal)}.btn-cyan{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-cyan-fg);--tblr-btn-bg:var(--tblr-cyan);--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-bg:rgba(var(--tblr-cyan-rgb), .8);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:rgba(var(--tblr-cyan-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-cyan);--tblr-btn-disabled-color:var(--tblr-cyan-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-cyan{--tblr-btn-color:var(--tblr-cyan);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-cyan);--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-cyan);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:var(--tblr-cyan);--tblr-btn-disabled-color:var(--tblr-cyan);--tblr-btn-disabled-border-color:var(--tblr-cyan)}.btn-facebook{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-facebook-fg);--tblr-btn-bg:var(--tblr-facebook);--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-bg:rgba(var(--tblr-facebook-rgb), .8);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:rgba(var(--tblr-facebook-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-facebook);--tblr-btn-disabled-color:var(--tblr-facebook-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-facebook{--tblr-btn-color:var(--tblr-facebook);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-facebook);--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-facebook);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:var(--tblr-facebook);--tblr-btn-disabled-color:var(--tblr-facebook);--tblr-btn-disabled-border-color:var(--tblr-facebook)}.btn-twitter{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-twitter-fg);--tblr-btn-bg:var(--tblr-twitter);--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-bg:rgba(var(--tblr-twitter-rgb), .8);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:rgba(var(--tblr-twitter-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-twitter);--tblr-btn-disabled-color:var(--tblr-twitter-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-twitter{--tblr-btn-color:var(--tblr-twitter);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-twitter);--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-twitter);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:var(--tblr-twitter);--tblr-btn-disabled-color:var(--tblr-twitter);--tblr-btn-disabled-border-color:var(--tblr-twitter)}.btn-linkedin{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-linkedin-fg);--tblr-btn-bg:var(--tblr-linkedin);--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-bg:rgba(var(--tblr-linkedin-rgb), .8);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:rgba(var(--tblr-linkedin-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-linkedin);--tblr-btn-disabled-color:var(--tblr-linkedin-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-linkedin{--tblr-btn-color:var(--tblr-linkedin);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-linkedin);--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-linkedin);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:var(--tblr-linkedin);--tblr-btn-disabled-color:var(--tblr-linkedin);--tblr-btn-disabled-border-color:var(--tblr-linkedin)}.btn-google{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-google-fg);--tblr-btn-bg:var(--tblr-google);--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-bg:rgba(var(--tblr-google-rgb), .8);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:rgba(var(--tblr-google-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-google);--tblr-btn-disabled-color:var(--tblr-google-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-google{--tblr-btn-color:var(--tblr-google);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-google);--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-google);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:var(--tblr-google);--tblr-btn-disabled-color:var(--tblr-google);--tblr-btn-disabled-border-color:var(--tblr-google)}.btn-youtube{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-youtube-fg);--tblr-btn-bg:var(--tblr-youtube);--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-bg:rgba(var(--tblr-youtube-rgb), .8);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:rgba(var(--tblr-youtube-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-youtube);--tblr-btn-disabled-color:var(--tblr-youtube-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-youtube{--tblr-btn-color:var(--tblr-youtube);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-youtube);--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-youtube);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:var(--tblr-youtube);--tblr-btn-disabled-color:var(--tblr-youtube);--tblr-btn-disabled-border-color:var(--tblr-youtube)}.btn-vimeo{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-vimeo-fg);--tblr-btn-bg:var(--tblr-vimeo);--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-bg:rgba(var(--tblr-vimeo-rgb), .8);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:rgba(var(--tblr-vimeo-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-vimeo);--tblr-btn-disabled-color:var(--tblr-vimeo-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-vimeo{--tblr-btn-color:var(--tblr-vimeo);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-vimeo);--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-vimeo);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:var(--tblr-vimeo);--tblr-btn-disabled-color:var(--tblr-vimeo);--tblr-btn-disabled-border-color:var(--tblr-vimeo)}.btn-dribbble{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-dribbble-fg);--tblr-btn-bg:var(--tblr-dribbble);--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-bg:rgba(var(--tblr-dribbble-rgb), .8);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:rgba(var(--tblr-dribbble-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-dribbble);--tblr-btn-disabled-color:var(--tblr-dribbble-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-dribbble{--tblr-btn-color:var(--tblr-dribbble);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-dribbble);--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-dribbble);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:var(--tblr-dribbble);--tblr-btn-disabled-color:var(--tblr-dribbble);--tblr-btn-disabled-border-color:var(--tblr-dribbble)}.btn-github{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-github-fg);--tblr-btn-bg:var(--tblr-github);--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-bg:rgba(var(--tblr-github-rgb), .8);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:rgba(var(--tblr-github-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-github);--tblr-btn-disabled-color:var(--tblr-github-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-github{--tblr-btn-color:var(--tblr-github);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-github);--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-github);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:var(--tblr-github);--tblr-btn-disabled-color:var(--tblr-github);--tblr-btn-disabled-border-color:var(--tblr-github)}.btn-instagram{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-instagram-fg);--tblr-btn-bg:var(--tblr-instagram);--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-bg:rgba(var(--tblr-instagram-rgb), .8);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:rgba(var(--tblr-instagram-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-instagram);--tblr-btn-disabled-color:var(--tblr-instagram-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-instagram{--tblr-btn-color:var(--tblr-instagram);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-instagram);--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-instagram);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:var(--tblr-instagram);--tblr-btn-disabled-color:var(--tblr-instagram);--tblr-btn-disabled-border-color:var(--tblr-instagram)}.btn-pinterest{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-pinterest-fg);--tblr-btn-bg:var(--tblr-pinterest);--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-bg:rgba(var(--tblr-pinterest-rgb), .8);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:rgba(var(--tblr-pinterest-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-pinterest);--tblr-btn-disabled-color:var(--tblr-pinterest-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-pinterest{--tblr-btn-color:var(--tblr-pinterest);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-pinterest);--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-pinterest);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:var(--tblr-pinterest);--tblr-btn-disabled-color:var(--tblr-pinterest);--tblr-btn-disabled-border-color:var(--tblr-pinterest)}.btn-vk{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-vk-fg);--tblr-btn-bg:var(--tblr-vk);--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-bg:rgba(var(--tblr-vk-rgb), .8);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:rgba(var(--tblr-vk-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-vk);--tblr-btn-disabled-color:var(--tblr-vk-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-vk{--tblr-btn-color:var(--tblr-vk);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-vk);--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-vk);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:var(--tblr-vk);--tblr-btn-disabled-color:var(--tblr-vk);--tblr-btn-disabled-border-color:var(--tblr-vk)}.btn-rss{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-rss-fg);--tblr-btn-bg:var(--tblr-rss);--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-bg:rgba(var(--tblr-rss-rgb), .8);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:rgba(var(--tblr-rss-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-rss);--tblr-btn-disabled-color:var(--tblr-rss-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-rss{--tblr-btn-color:var(--tblr-rss);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-rss);--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-rss);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:var(--tblr-rss);--tblr-btn-disabled-color:var(--tblr-rss);--tblr-btn-disabled-border-color:var(--tblr-rss)}.btn-flickr{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-flickr-fg);--tblr-btn-bg:var(--tblr-flickr);--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-bg:rgba(var(--tblr-flickr-rgb), .8);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:rgba(var(--tblr-flickr-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-flickr);--tblr-btn-disabled-color:var(--tblr-flickr-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-flickr{--tblr-btn-color:var(--tblr-flickr);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-flickr);--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-flickr);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:var(--tblr-flickr);--tblr-btn-disabled-color:var(--tblr-flickr);--tblr-btn-disabled-border-color:var(--tblr-flickr)}.btn-bitbucket{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-bitbucket-fg);--tblr-btn-bg:var(--tblr-bitbucket);--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-bg:rgba(var(--tblr-bitbucket-rgb), .8);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:rgba(var(--tblr-bitbucket-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-bitbucket);--tblr-btn-disabled-color:var(--tblr-bitbucket-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-bitbucket{--tblr-btn-color:var(--tblr-bitbucket);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-bitbucket);--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-bitbucket);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:var(--tblr-bitbucket);--tblr-btn-disabled-color:var(--tblr-bitbucket);--tblr-btn-disabled-border-color:var(--tblr-bitbucket)}.btn-tabler{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-tabler-fg);--tblr-btn-bg:var(--tblr-tabler);--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-bg:rgba(var(--tblr-tabler-rgb), .8);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:rgba(var(--tblr-tabler-rgb), .8);--tblr-btn-disabled-bg:var(--tblr-tabler);--tblr-btn-disabled-color:var(--tblr-tabler-fg);--tblr-btn-box-shadow:var(--tblr-shadow-button),var(--tblr-shadow-button-inset)}.btn-outline-tabler{--tblr-btn-color:var(--tblr-tabler);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-tabler);--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-tabler);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:var(--tblr-tabler);--tblr-btn-disabled-color:var(--tblr-tabler);--tblr-btn-disabled-border-color:var(--tblr-tabler)}.btn-ghost-primary{--tblr-btn-color:var(--tblr-primary);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-bg:var(--tblr-primary);--tblr-btn-hover-border-color:var(--tblr-primary);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:var(--tblr-primary);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-primary);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-secondary{--tblr-btn-color:var(--tblr-secondary);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-bg:var(--tblr-secondary);--tblr-btn-hover-border-color:var(--tblr-secondary);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:var(--tblr-secondary);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-secondary);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-success{--tblr-btn-color:var(--tblr-success);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-bg:var(--tblr-success);--tblr-btn-hover-border-color:var(--tblr-success);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:var(--tblr-success);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-success);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-info{--tblr-btn-color:var(--tblr-info);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-bg:var(--tblr-info);--tblr-btn-hover-border-color:var(--tblr-info);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:var(--tblr-info);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-info);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-warning{--tblr-btn-color:var(--tblr-warning);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-bg:var(--tblr-warning);--tblr-btn-hover-border-color:var(--tblr-warning);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:var(--tblr-warning);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-warning);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-danger{--tblr-btn-color:var(--tblr-danger);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-bg:var(--tblr-danger);--tblr-btn-hover-border-color:var(--tblr-danger);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:var(--tblr-danger);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-danger);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-light{--tblr-btn-color:var(--tblr-light);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-bg:var(--tblr-light);--tblr-btn-hover-border-color:var(--tblr-light);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:var(--tblr-light);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-light);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-dark{--tblr-btn-color:var(--tblr-dark);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-bg:var(--tblr-dark);--tblr-btn-hover-border-color:var(--tblr-dark);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:var(--tblr-dark);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-dark);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-muted{--tblr-btn-color:var(--tblr-muted);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-bg:var(--tblr-muted);--tblr-btn-hover-border-color:var(--tblr-muted);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:var(--tblr-muted);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-muted);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-blue{--tblr-btn-color:var(--tblr-blue);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-bg:var(--tblr-blue);--tblr-btn-hover-border-color:var(--tblr-blue);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:var(--tblr-blue);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-blue);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-azure{--tblr-btn-color:var(--tblr-azure);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-bg:var(--tblr-azure);--tblr-btn-hover-border-color:var(--tblr-azure);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:var(--tblr-azure);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-azure);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-indigo{--tblr-btn-color:var(--tblr-indigo);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-bg:var(--tblr-indigo);--tblr-btn-hover-border-color:var(--tblr-indigo);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:var(--tblr-indigo);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-indigo);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-purple{--tblr-btn-color:var(--tblr-purple);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-bg:var(--tblr-purple);--tblr-btn-hover-border-color:var(--tblr-purple);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:var(--tblr-purple);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-purple);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-pink{--tblr-btn-color:var(--tblr-pink);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-bg:var(--tblr-pink);--tblr-btn-hover-border-color:var(--tblr-pink);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:var(--tblr-pink);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-pink);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-red{--tblr-btn-color:var(--tblr-red);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-bg:var(--tblr-red);--tblr-btn-hover-border-color:var(--tblr-red);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:var(--tblr-red);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-red);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-orange{--tblr-btn-color:var(--tblr-orange);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-bg:var(--tblr-orange);--tblr-btn-hover-border-color:var(--tblr-orange);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:var(--tblr-orange);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-orange);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-yellow{--tblr-btn-color:var(--tblr-yellow);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-bg:var(--tblr-yellow);--tblr-btn-hover-border-color:var(--tblr-yellow);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:var(--tblr-yellow);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-yellow);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-lime{--tblr-btn-color:var(--tblr-lime);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-bg:var(--tblr-lime);--tblr-btn-hover-border-color:var(--tblr-lime);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:var(--tblr-lime);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-lime);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-green{--tblr-btn-color:var(--tblr-green);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-bg:var(--tblr-green);--tblr-btn-hover-border-color:var(--tblr-green);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:var(--tblr-green);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-green);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-teal{--tblr-btn-color:var(--tblr-teal);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-bg:var(--tblr-teal);--tblr-btn-hover-border-color:var(--tblr-teal);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:var(--tblr-teal);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-teal);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-cyan{--tblr-btn-color:var(--tblr-cyan);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-bg:var(--tblr-cyan);--tblr-btn-hover-border-color:var(--tblr-cyan);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:var(--tblr-cyan);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-cyan);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-facebook{--tblr-btn-color:var(--tblr-facebook);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-bg:var(--tblr-facebook);--tblr-btn-hover-border-color:var(--tblr-facebook);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:var(--tblr-facebook);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-facebook);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-twitter{--tblr-btn-color:var(--tblr-twitter);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-bg:var(--tblr-twitter);--tblr-btn-hover-border-color:var(--tblr-twitter);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:var(--tblr-twitter);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-twitter);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-linkedin{--tblr-btn-color:var(--tblr-linkedin);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-bg:var(--tblr-linkedin);--tblr-btn-hover-border-color:var(--tblr-linkedin);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:var(--tblr-linkedin);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-linkedin);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-google{--tblr-btn-color:var(--tblr-google);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-bg:var(--tblr-google);--tblr-btn-hover-border-color:var(--tblr-google);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:var(--tblr-google);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-google);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-youtube{--tblr-btn-color:var(--tblr-youtube);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-bg:var(--tblr-youtube);--tblr-btn-hover-border-color:var(--tblr-youtube);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:var(--tblr-youtube);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-youtube);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-vimeo{--tblr-btn-color:var(--tblr-vimeo);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-bg:var(--tblr-vimeo);--tblr-btn-hover-border-color:var(--tblr-vimeo);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:var(--tblr-vimeo);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-vimeo);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-dribbble{--tblr-btn-color:var(--tblr-dribbble);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-bg:var(--tblr-dribbble);--tblr-btn-hover-border-color:var(--tblr-dribbble);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:var(--tblr-dribbble);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-dribbble);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-github{--tblr-btn-color:var(--tblr-github);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-bg:var(--tblr-github);--tblr-btn-hover-border-color:var(--tblr-github);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:var(--tblr-github);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-github);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-instagram{--tblr-btn-color:var(--tblr-instagram);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-bg:var(--tblr-instagram);--tblr-btn-hover-border-color:var(--tblr-instagram);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:var(--tblr-instagram);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-instagram);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-pinterest{--tblr-btn-color:var(--tblr-pinterest);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-bg:var(--tblr-pinterest);--tblr-btn-hover-border-color:var(--tblr-pinterest);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:var(--tblr-pinterest);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-pinterest);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-vk{--tblr-btn-color:var(--tblr-vk);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-bg:var(--tblr-vk);--tblr-btn-hover-border-color:var(--tblr-vk);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:var(--tblr-vk);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-vk);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-rss{--tblr-btn-color:var(--tblr-rss);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-bg:var(--tblr-rss);--tblr-btn-hover-border-color:var(--tblr-rss);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:var(--tblr-rss);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-rss);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-flickr{--tblr-btn-color:var(--tblr-flickr);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-bg:var(--tblr-flickr);--tblr-btn-hover-border-color:var(--tblr-flickr);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:var(--tblr-flickr);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-flickr);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-bitbucket{--tblr-btn-color:var(--tblr-bitbucket);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-bg:var(--tblr-bitbucket);--tblr-btn-hover-border-color:var(--tblr-bitbucket);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:var(--tblr-bitbucket);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-bitbucket);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-ghost-tabler{--tblr-btn-color:var(--tblr-tabler);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-bg:var(--tblr-tabler);--tblr-btn-hover-border-color:var(--tblr-tabler);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:var(--tblr-tabler);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-tabler);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-group-sm>.btn,.btn-sm{--tblr-btn-line-height:1.5;--tblr-btn-icon-size:.75rem}.btn-group-lg>.btn,.btn-lg{--tblr-btn-line-height:1.5;--tblr-btn-icon-size:2rem}.btn-pill{padding-right:1.5em;padding-left:1.5em;border-radius:10rem}.btn-pill[class*=btn-icon]{padding:.375rem 15px}.btn-square{border-radius:0}.btn-icon{min-width:calc(var(--tblr-btn-line-height) * var(--tblr-btn-font-size) + var(--tblr-btn-padding-y) * 2 + var(--tblr-btn-border-width) * 2);min-height:calc(var(--tblr-btn-line-height) * var(--tblr-btn-font-size) + var(--tblr-btn-padding-y) * 2 + var(--tblr-btn-border-width) * 2);padding-left:0;padding-right:0}.btn-icon .icon{margin:calc(-1 * var(--tblr-btn-padding-x))}.btn-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.btn-floating{position:fixed;z-index:1030;bottom:1.5rem;right:1.5rem;border-radius:100rem}.btn-loading{position:relative;color:transparent!important;text-shadow:none!important;pointer-events:none}.btn-loading>*{opacity:0}.btn-loading:after{content:"";display:inline-block;vertical-align:text-bottom;border:2px var(--tblr-border-style) currentColor;border-right-color:transparent;border-radius:100rem;color:var(--tblr-btn-color);position:absolute;width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);left:calc(50% - var(--tblr-btn-icon-size)/ 2);top:calc(50% - var(--tblr-btn-icon-size)/ 2);animation:spinner-border .75s linear infinite}.btn-action{padding:0;border:0;color:var(--tblr-muted);display:inline-flex;width:2rem;height:2rem;align-items:center;justify-content:center;border-radius:var(--tblr-border-radius);background:0 0}.btn-action:after{content:none}.btn-action:focus{outline:0;box-shadow:none}.btn-action.show,.btn-action:hover{color:var(--tblr-body-color);background:var(--tblr-active-bg)}.btn-action.show{color:var(--tblr-primary)}.btn-action .icon{margin:0;width:1.25rem;height:1.25rem;font-size:1.25rem;stroke-width:1}.btn-actions{display:flex}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group>.btn-check:checked+.btn,.btn-group>.btn.active,.btn-group>.btn:active{z-index:5}.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.calendar{display:block;font-size:.765625rem;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.calendar-nav{display:flex;align-items:center}.calendar-title{flex:1;text-align:center}.calendar-body,.calendar-header{display:flex;flex-wrap:wrap;justify-content:flex-start;padding:.5rem 0}.calendar-header{color:var(--tblr-muted)}.calendar-date{flex:0 0 14.2857142857%;max-width:14.2857142857%;padding:.2rem;text-align:center;border:0}.calendar-date.next-month,.calendar-date.prev-month{opacity:.25}.calendar-date .date-item{position:relative;display:inline-block;width:1.4rem;height:1.4rem;line-height:1.4rem;color:#66758c;text-align:center;text-decoration:none;white-space:nowrap;vertical-align:middle;cursor:pointer;background:0 0;border:var(--tblr-border-width) var(--tblr-border-style) transparent;border-radius:100rem;outline:0;transition:background .3s,border .3s,box-shadow .32s,color .3s}@media (prefers-reduced-motion:reduce){.calendar-date .date-item{transition:none}}.calendar-date .date-item:hover{color:var(--tblr-primary);text-decoration:none;background:#fefeff;border-color:var(--tblr-border-color)}.calendar-date .date-today{color:var(--tblr-primary);border-color:var(--tblr-border-color)}.calendar-range{position:relative}.calendar-range:before{position:absolute;top:50%;right:0;left:0;height:1.4rem;content:"";background:rgba(var(--tblr-primary-rgb),.1);transform:translateY(-50%)}.calendar-range.range-end .date-item,.calendar-range.range-start .date-item{color:#fff;background:var(--tblr-primary);border-color:var(--tblr-primary)}.calendar-range.range-start:before{left:50%}.calendar-range.range-end:before{right:50%}.carousel-indicators-vertical{left:auto;top:0;margin:0 1rem 0 0;flex-direction:column}.carousel-indicators-vertical [data-bs-target]{margin:3px 0 3px;width:3px;height:30px;border:0;border-left:10px var(--tblr-border-style) transparent;border-right:10px var(--tblr-border-style) transparent}.carousel-indicators-dot [data-bs-target]{width:.5rem;height:.5rem;border-radius:100rem;border:10px var(--tblr-border-style) transparent;margin:0}.carousel-indicators-thumb [data-bs-target]{width:2rem;height:auto;background:no-repeat center/cover;border:0;border-radius:var(--tblr-border-radius);box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px 0;margin:0 3px;opacity:.75}@media (min-width:992px){.carousel-indicators-thumb [data-bs-target]{width:4rem}}.carousel-indicators-thumb [data-bs-target]:before{content:"";padding-top:var(--tblr-aspect-ratio,100%);display:block}.carousel-indicators-thumb.carousel-indicators-vertical [data-bs-target]{margin:3px 0}.carousel-caption-background{background:red;position:absolute;left:0;right:0;bottom:0;height:90%;background:linear-gradient(0deg,rgba(29,39,59,.9),rgba(29,39,59,0))}.card{transition:transform .3s ease-out,opacity .3s ease-out,box-shadow .3s ease-out}@media (prefers-reduced-motion:reduce){.card{transition:none}}@media print{.card{border:none;box-shadow:none}}a.card{color:inherit}a.card:hover{text-decoration:none;box-shadow:rgba(var(--tblr-body-color-rgb),.16) 0 2px 16px 0}.card .card{box-shadow:none}.card-borderless,.card-borderless .card-footer,.card-borderless .card-header{border-color:transparent}.card-stamp{--tblr-stamp-size:7rem;position:absolute;top:0;right:0;width:calc(var(--tblr-stamp-size) * 1);height:calc(var(--tblr-stamp-size) * 1);max-height:100%;border-top-right-radius:4px;opacity:.2;overflow:hidden;pointer-events:none}.card-stamp-lg{--tblr-stamp-size:13rem}.card-stamp-icon{background:var(--tblr-muted);color:var(--tblr-card-bg,var(--tblr-bg-surface));display:flex;align-items:center;justify-content:center;border-radius:100rem;width:calc(var(--tblr-stamp-size) * 1);height:calc(var(--tblr-stamp-size) * 1);position:relative;top:calc(var(--tblr-stamp-size) * -.25);right:calc(var(--tblr-stamp-size) * -.25);font-size:calc(var(--tblr-stamp-size) * .75);transform:rotate(10deg)}.card-stamp-icon .icon{stroke-width:2;width:calc(var(--tblr-stamp-size) * .75);height:calc(var(--tblr-stamp-size) * .75)}.card-img,.card-img-start{border-top-left-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)));border-bottom-left-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)))}.card-img,.card-img-end{border-top-right-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)));border-bottom-right-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)))}.card-img-overlay{display:flex;flex-direction:column;justify-content:flex-end}.card-img-overlay-dark{background-image:linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,.6) 100%)}.card-inactive{pointer-events:none;box-shadow:none}.card-inactive .card-body{opacity:.64}.card-active{--tblr-card-border-color:var(--tblr-primary);--tblr-card-bg:var(--tblr-active-bg)}.card-btn{display:flex;align-items:center;justify-content:center;padding:1rem 1.5rem;text-align:center;transition:background .3s;border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);flex:1;color:inherit;font-weight:var(--tblr-font-weight-medium)}@media (prefers-reduced-motion:reduce){.card-btn{transition:none}}.card-btn:hover{text-decoration:none;background:rgba(var(--tblr-primary-rgb),.04)}.card-btn+.card-btn{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-stacked{--tblr-card-stacked-offset:.25rem;position:relative}.card-stacked:after{position:absolute;top:calc(-1 * var(--tblr-card-stacked-offset));right:var(--tblr-card-stacked-offset);left:var(--tblr-card-stacked-offset);height:var(--tblr-card-stacked-offset);content:"";background:var(--tblr-card-bg,var(--tblr-bg-surface));border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-card-border-color);border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-cover{position:relative;padding:1rem 1.5rem;background:#666 no-repeat center/cover}.card-cover:before{position:absolute;top:0;right:0;bottom:0;left:0;content:"";background:rgba(29,39,59,.48)}.card-cover:first-child,.card-cover:first-child:before{border-radius:4px 4px 0 0}.card-cover-blurred:before{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.card-actions{margin:-.5rem -.5rem -.5rem auto;padding-left:.5rem}.card-actions a{text-decoration:none}.card-header{color:inherit;display:flex;align-items:center;background:0 0}.card-header:first-child{border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-header-light{border-bottom-color:transparent;background:var(--tblr-bg-surface-secondary)}.card-header-tabs{background:var(--tblr-bg-surface-secondary);flex:1;margin:calc(var(--tblr-card-cap-padding-y) * -1) calc(var(--tblr-card-cap-padding-x) * -1) calc(var(--tblr-card-cap-padding-y) * -1);padding:calc(var(--tblr-card-cap-padding-y) * .5) calc(var(--tblr-card-cap-padding-x) * .5) 0}.card-header-pills{flex:1;margin-top:-.5rem;margin-bottom:-.5rem}.card-rotate-left{transform:rotate(-1.5deg)}.card-rotate-right{transform:rotate(1.5deg)}.card-link{color:inherit}.card-link:hover{color:inherit;text-decoration:none;box-shadow:0 1px 6px 0 rgba(0,0,0,.08)}.card-link-rotate:hover{transform:rotate(1.5deg);opacity:1}.card-link-pop:hover{transform:translateY(-2px);opacity:1}.card-footer{margin-top:auto}.card-footer:last-child{border-radius:0 0 var(--tblr-card-border-radius) var(--tblr-card-border-radius)}.card-footer-transparent{background:0 0;border-color:transparent;padding-top:0}.card-footer-borderless{border-top:none}.card-progress{height:.25rem}.card-progress:last-child{border-radius:0 0 2px 2px}.card-progress:first-child{border-radius:2px 2px 0 0}.card-meta{color:var(--tblr-muted)}.card-title{display:block;margin:0 0 1rem;font-size:1rem;font-weight:var(--tblr-font-weight-medium);line-height:1.5rem}a.card-title:hover{color:inherit}.card-header .card-title{margin:0}.card-subtitle{margin-bottom:1.25rem;color:var(--tblr-muted);font-weight:400}.card-header .card-subtitle{margin:0}.card-title .card-subtitle{margin:0 0 0 .25rem;font-size:.875rem}.card-body{position:relative}.card-body>:last-child{margin-bottom:0}.card-sm>.card-body{padding:1rem}@media (min-width:768px){.card-md>.card-body{padding:2.5rem}}@media (min-width:768px){.card-lg>.card-body{padding:2rem}}@media (min-width:992px){.card-lg>.card-body{padding:4rem}}@media print{.card-body{padding:0}}.card-body+.card-body{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-body-scrollable{overflow:auto}.card-options{top:1.5rem;right:.75rem;display:flex;margin-left:auto}.card-options-link{display:inline-block;min-width:1rem;margin-left:.25rem;color:var(--tblr-muted)}.card-status-top{position:absolute;top:0;right:0;left:0;height:2px;border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-status-start{position:absolute;right:auto;bottom:0;width:2px;height:100%;border-radius:var(--tblr-card-border-radius) 0 0 var(--tblr-card-border-radius)}.card-status-bottom{position:absolute;top:initial;bottom:0;width:100%;height:2px;border-radius:0 0 var(--tblr-card-border-radius) var(--tblr-card-border-radius)}.card-table{margin-bottom:0!important}.card-table tr td:first-child,.card-table tr th:first-child{padding-left:1.5rem;border-left:0}.card-table tr td:last-child,.card-table tr th:last-child{padding-right:1.5rem;border-right:0}.card-table tbody tr:first-child,.card-table tfoot tr:first-child,.card-table thead tr:first-child{border-top:0}.card-table tbody tr:first-child td,.card-table tbody tr:first-child th,.card-table tfoot tr:first-child td,.card-table tfoot tr:first-child th,.card-table thead tr:first-child td,.card-table thead tr:first-child th{border-top:0}.card-body+.card-table{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-table-border-color)}.card-code{padding:0}.card-code .highlight{margin:0;border:0}.card-code pre{margin:0!important;border:0!important}.card-chart{position:relative;z-index:1;height:3.5rem}.card-avatar{margin-left:auto;margin-right:auto;box-shadow:0 0 0 .25rem var(--tblr-card-bg,var(--tblr-bg-surface));margin-top:calc(-1 * var(--tblr-avatar-size) * .5)}.card-body+.card-list-group{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-list-group .list-group-item{padding-right:1.5rem;padding-left:1.5rem;border-right:0;border-left:0;border-radius:0}.card-list-group .list-group-item:last-child{border-bottom:0}.card-list-group .list-group-item:first-child{border-top:0}.card-tabs .nav-tabs{position:relative;z-index:1000;border-bottom:0}.card-tabs .nav-tabs .nav-link{background:var(--tblr-bg-surface-secondary);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-tabs .nav-tabs .nav-link.active,.card-tabs .nav-tabs .nav-link:active,.card-tabs .nav-tabs .nav-link:hover{border-color:var(--tblr-border-color);color:#1d273b}.card-tabs .nav-tabs .nav-link.active{background:var(--tblr-card-bg,var(--tblr-bg-surface));border-bottom-color:transparent}.card-tabs .nav-tabs .nav-item:not(:first-child) .nav-link{border-top-left-radius:0}.card-tabs .nav-tabs .nav-item:not(:last-child) .nav-link{border-top-right-radius:0}.card-tabs .nav-tabs .nav-item+.nav-item{margin-left:calc(-1 * var(--tblr-border-width))}.card-tabs .nav-tabs-bottom{margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-link{margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-link.active{border-top-color:transparent}.card-tabs .nav-tabs-bottom .nav-item{margin-top:calc(-1 * var(--tblr-border-width));margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-item .nav-link{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:0 0 var(--tblr-border-radius) var(--tblr-border-radius)}.card-tabs .nav-tabs-bottom .nav-item:not(:first-child) .nav-link{border-bottom-left-radius:0}.card-tabs .nav-tabs-bottom .nav-item:not(:last-child) .nav-link{border-bottom-right-radius:0}.card-tabs .card{border-bottom-left-radius:0}.card-tabs .nav-tabs+.tab-content .card{border-bottom-left-radius:var(--tblr-card-border-radius);border-top-left-radius:0}.btn-close{cursor:pointer}.btn-close:focus{outline:0}.dropdown-menu{box-shadow:0 .5rem 1rem rgba(0,0,0,.15);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin:0!important}.dropdown-menu.card{padding:0;min-width:25rem;display:none}.dropdown-menu.card.show{display:flex}.dropdown-item{min-width:11rem;display:flex;align-items:center;margin:0;line-height:1.4285714286}.dropdown-item-icon{width:1.25rem!important;height:1.25rem!important;margin-right:.5rem;color:var(--tblr-muted);opacity:.7;text-align:center}.dropdown-item-indicator{margin-right:.5rem;margin-left:-.25rem;height:1.25rem;display:inline-flex;line-height:1;vertical-align:bottom;align-items:center}.dropdown-header{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);padding-bottom:.25rem;pointer-events:none}.dropdown-menu-scrollable{height:auto;max-height:13rem;overflow-x:hidden}.dropdown-menu-column{min-width:11rem}.dropdown-menu-column .dropdown-item{min-width:0}.dropdown-menu-columns{display:flex;flex:0 .25rem}.dropdown-menu-arrow:before{content:"";position:absolute;top:-.25rem;left:.75rem;display:block;background:inherit;width:14px;height:14px;transform:rotate(45deg);transform-origin:center;border:1px solid;border-color:inherit;z-index:-1;clip:rect(0,9px,9px,0)}.dropdown-menu-arrow.dropdown-menu-end:before{right:.75rem;left:auto}.dropend>.dropdown-menu{margin-top:calc(-.25rem - 1px);margin-left:-.25rem}.dropend .dropdown-toggle:after{margin-left:auto}.dropdown-menu-card{padding:0}.dropdown-menu-card>.card{margin:0;border:0;box-shadow:none}.datagrid{--tblr-datagrid-padding:1.5rem;--tblr-datagrid-item-width:15rem;display:grid;grid-gap:var(--tblr-datagrid-padding);grid-template-columns:repeat(auto-fit,minmax(var(--tblr-datagrid-item-width),1fr))}.datagrid-title{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);margin-bottom:.25rem}.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:1rem;text-align:center}@media (min-width:768px){.empty{padding:3rem}}.empty-icon{margin:0 0 1rem;width:3rem;height:3rem;line-height:1;color:var(--tblr-muted)}.empty-icon svg{width:100%;height:100%}.empty-img{margin:0 0 2rem;line-height:1}.empty-img img{height:8rem;width:auto}.empty-header{margin:0 0 1rem;font-size:4rem;font-weight:var(--tblr-font-weight-light);line-height:1;color:var(--tblr-muted)}.empty-title{font-size:1.25rem;line-height:1.75rem;font-weight:var(--tblr-font-weight-medium)}.empty-subtitle,.empty-title{margin:0 0 .5rem}.empty-action{margin-top:1.5rem}.empty-bordered{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.row>*{min-width:0}.col-separator{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}@media (max-width:991.98px){.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--tblr-gutter-x:1rem}}.container-tight{--tblr-gutter-x:1.5rem;--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto;max-width:30rem}.container-narrow{--tblr-gutter-x:1.5rem;--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto;max-width:45rem}.row-0{margin-right:0;margin-left:0}.row-0>.col,.row-0>[class*=col-]{padding-right:0;padding-left:0}.row-0 .card{margin-bottom:0}.row-sm{margin-right:-.375rem;margin-left:-.375rem}.row-sm>.col,.row-sm>[class*=col-]{padding-right:.375rem;padding-left:.375rem}.row-sm .card{margin-bottom:.75rem}.row-md{margin-right:-1.5rem;margin-left:-1.5rem}.row-md>.col,.row-md>[class*=col-]{padding-right:1.5rem;padding-left:1.5rem}.row-md .card{margin-bottom:3rem}.row-lg{margin-right:-3rem;margin-left:-3rem}.row-lg>.col,.row-lg>[class*=col-]{padding-right:3rem;padding-left:3rem}.row-lg .card{margin-bottom:6rem}.row-deck>.col,.row-deck>[class*=col-]{display:flex;align-items:stretch}.row-deck>.col .card,.row-deck>[class*=col-] .card{flex:1 1 auto}.row-cards{--tblr-gutter-x:1rem;--tblr-gutter-y:1rem;min-width:0}.row-cards .row-cards{flex:1}@media (max-width:991.98px){.row-cards{--tblr-gutter-x:0.5rem;--tblr-gutter-y:0.5rem}}.space-y{display:flex;flex-direction:column;gap:1rem}.space-x{display:flex;gap:1rem}.space-y-0{display:flex;flex-direction:column;gap:0}.space-x-0{display:flex;gap:0}.space-y-1{display:flex;flex-direction:column;gap:.25rem}.space-x-1{display:flex;gap:.25rem}.space-y-2{display:flex;flex-direction:column;gap:.5rem}.space-x-2{display:flex;gap:.5rem}.space-y-3{display:flex;flex-direction:column;gap:1rem}.space-x-3{display:flex;gap:1rem}.space-y-4{display:flex;flex-direction:column;gap:2rem}.space-x-4{display:flex;gap:2rem}.space-y-5{display:flex;flex-direction:column;gap:4rem}.space-x-5{display:flex;gap:4rem}.divide-y>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y>:not(template):not(:first-child){padding-top:1rem!important}.divide-y>:not(template):not(:last-child){padding-bottom:1rem!important}.divide-x>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x>:not(template):not(:first-child){padding-left:1rem!important}.divide-x>:not(template):not(:last-child){padding-right:1rem!important}.divide-y-0>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-0>:not(template):not(:first-child){padding-top:0!important}.divide-y-0>:not(template):not(:last-child){padding-bottom:0!important}.divide-x-0>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-0>:not(template):not(:first-child){padding-left:0!important}.divide-x-0>:not(template):not(:last-child){padding-right:0!important}.divide-y-1>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-1>:not(template):not(:first-child){padding-top:.25rem!important}.divide-y-1>:not(template):not(:last-child){padding-bottom:.25rem!important}.divide-x-1>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-1>:not(template):not(:first-child){padding-left:.25rem!important}.divide-x-1>:not(template):not(:last-child){padding-right:.25rem!important}.divide-y-2>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-2>:not(template):not(:first-child){padding-top:.5rem!important}.divide-y-2>:not(template):not(:last-child){padding-bottom:.5rem!important}.divide-x-2>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-2>:not(template):not(:first-child){padding-left:.5rem!important}.divide-x-2>:not(template):not(:last-child){padding-right:.5rem!important}.divide-y-3>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-3>:not(template):not(:first-child){padding-top:1rem!important}.divide-y-3>:not(template):not(:last-child){padding-bottom:1rem!important}.divide-x-3>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-3>:not(template):not(:first-child){padding-left:1rem!important}.divide-x-3>:not(template):not(:last-child){padding-right:1rem!important}.divide-y-4>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-4>:not(template):not(:first-child){padding-top:2rem!important}.divide-y-4>:not(template):not(:last-child){padding-bottom:2rem!important}.divide-x-4>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-4>:not(template):not(:first-child){padding-left:2rem!important}.divide-x-4>:not(template):not(:last-child){padding-right:2rem!important}.divide-y-5>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-y-5>:not(template):not(:first-child){padding-top:4rem!important}.divide-y-5>:not(template):not(:last-child){padding-bottom:4rem!important}.divide-x-5>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)!important}.divide-x-5>:not(template):not(:first-child){padding-left:4rem!important}.divide-x-5>:not(template):not(:last-child){padding-right:4rem!important}.divide-y-fill{display:flex;flex-direction:column;height:100%}.divide-y-fill>:not(template){flex:1;display:flex;justify-content:center;flex-direction:column}.icon{--tblr-icon-size:1.25rem;width:var(--tblr-icon-size);height:var(--tblr-icon-size);font-size:var(--tblr-icon-size);vertical-align:bottom;stroke-width:1.5}.icon:hover{text-decoration:none}.icon-inline{--tblr-icon-size:1rem;vertical-align:-.2rem}.icon-filled{fill:currentColor}.icon-sm{--tblr-icon-size:1rem;stroke-width:1}.icon-md{--tblr-icon-size:2.5rem;stroke-width:1}.icon-lg{--tblr-icon-size:3.5rem;stroke-width:1}.icon-pulse{transition:all .15s ease 0s;animation:pulse 2s ease infinite;animation-fill-mode:both}.icon-tada{transition:all .15s ease 0s;animation:tada 3s ease infinite;animation-fill-mode:both}.icon-rotate{transition:all .15s ease 0s;animation:rotate-360 3s linear infinite;animation-fill-mode:both}.img-responsive{--tblr-img-responsive-ratio:75%;background:no-repeat center/cover;padding-top:var(--tblr-img-responsive-ratio)}.img-responsive-grid{padding-top:calc(var(--tblr-img-responsive-ratio) - var(--tblr-gutter-y)/ 2)}.img-responsive-1x1{--tblr-img-responsive-ratio:100%}.img-responsive-2x1{--tblr-img-responsive-ratio:50%}.img-responsive-1x2{--tblr-img-responsive-ratio:200%}.img-responsive-3x1{--tblr-img-responsive-ratio:33.3333333333%}.img-responsive-1x3{--tblr-img-responsive-ratio:300%}.img-responsive-4x3{--tblr-img-responsive-ratio:75%}.img-responsive-3x4{--tblr-img-responsive-ratio:133.3333333333%}.img-responsive-16x9{--tblr-img-responsive-ratio:56.25%}.img-responsive-9x16{--tblr-img-responsive-ratio:177.7777777778%}.img-responsive-21x9{--tblr-img-responsive-ratio:42.8571428571%}.img-responsive-9x21{--tblr-img-responsive-ratio:233.3333333333%}textarea[cols]{height:auto}.col-form-label,.form-label{display:block;font-weight:var(--tblr-font-weight-medium)}.col-form-label.required:after,.form-label.required:after{content:"*";margin-left:.25rem;color:#d63939}.form-label-description{float:right;font-weight:var(--tblr-font-weight-normal);color:var(--tblr-muted)}.form-hint{display:block;color:var(--tblr-muted)}.form-hint:last-child{margin-bottom:0}.form-hint+.form-control{margin-top:.25rem}.form-label+.form-hint{margin-top:-.25rem}.form-control+.form-hint,.form-select+.form-hint,.input-group+.form-hint{margin-top:.5rem}.form-select:-moz-focusring{color:var(--tblr-body-color)}.form-control:-webkit-autofill{box-shadow:0 0 0 1000px var(--tblr-body-bg) inset;color:var(--tblr-body-color)}.form-control.disabled,.form-control:disabled{color:var(--tblr-muted);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.form-control[size]{width:auto}.form-control-light{background-color:#f1f5f9;border-color:transparent}.form-control-dark{background-color:rgba(0,0,0,.1);color:#fff;border-color:transparent}.form-control-dark:focus{background-color:rgba(0,0,0,.1);box-shadow:none;border-color:rgba(255,255,255,.24)}.form-control-dark::-webkit-input-placeholder{color:rgba(255,255,255,.6)}.form-control-dark::-moz-placeholder{color:rgba(255,255,255,.6)}.form-control-dark:-ms-input-placeholder{color:rgba(255,255,255,.6)}.form-control-dark::-ms-input-placeholder{color:rgba(255,255,255,.6)}.form-control-dark::placeholder{color:rgba(255,255,255,.6)}.form-control-rounded{border-radius:10rem}.form-control-flush{padding:0;background:0 0!important;border-color:transparent!important;resize:none;box-shadow:none!important;line-height:inherit}.form-footer{margin-top:2rem}.form-fieldset{padding:1rem;margin-bottom:1rem;background:var(--tblr-body-bg);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.form-help{display:inline-flex;font-weight:var(--tblr-font-weight-bold);align-items:center;justify-content:center;width:1.125rem;height:1.125rem;font-size:.75rem;color:var(--tblr-muted);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:var(--tblr-gray-100);border-radius:100rem;transition:background-color .3s,color .3s}@media (prefers-reduced-motion:reduce){.form-help{transition:none}}.form-help:hover,.form-help[aria-describedby]{color:#fff;background:var(--tblr-primary)}.input-group-link{font-size:.75rem}.input-group-flat:focus-within{box-shadow:0 0 0 .25rem rgba(32,107,196,.25);border-radius:var(--tblr-border-radius)}.input-group-flat:focus-within .form-control,.input-group-flat:focus-within .input-group-text{border-color:#90b5e2!important}.input-group-flat .form-control:focus{border-color:var(--tblr-border-color);box-shadow:none}.input-group-flat .form-control:not(:last-child){border-right:0}.input-group-flat .form-control:not(:first-child){border-left:0}.input-group-flat .input-group-text{background:var(--tblr-bg-forms);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.input-group-flat .input-group-text{transition:none}}.input-group-flat .input-group-text:first-child{padding-right:0}.input-group-flat .input-group-text:last-child{padding-left:0}.form-file-button{margin-left:0;border-left:0}.input-icon{position:relative}.input-icon .form-control:not(:last-child),.input-icon .form-select:not(:last-child){padding-right:2.5rem}.input-icon .form-control:not(:first-child),.input-icon .form-select:not(:last-child){padding-left:2.5rem}.input-icon-addon{position:absolute;top:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;min-width:2.5rem;color:var(--tblr-icon-color);pointer-events:none;font-size:1.2em}.input-icon-addon:last-child{right:0;left:auto}.form-colorinput{position:relative;display:inline-block;margin:0;line-height:1;cursor:pointer}.form-colorinput-input{position:absolute;z-index:-1;opacity:0}.form-colorinput-color{display:block;width:1.5rem;height:1.5rem;color:#fff;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);border-radius:3px;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.form-colorinput-color:before{position:absolute;top:0;left:0;width:100%;height:100%;content:"";background:no-repeat center center/1rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e");opacity:0;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-colorinput-color:before{transition:none}}.form-colorinput-input:checked~.form-colorinput-color:before{opacity:1}.form-colorinput-input:focus~.form-colorinput-color{border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-colorinput-light .form-colorinput-color:before{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%231d273b' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-imagecheck{position:relative;margin:0;cursor:pointer}.form-imagecheck-input{position:absolute;z-index:-1;opacity:0}.form-imagecheck-figure{position:relative;display:block;margin:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:3px}.form-imagecheck-input:focus~.form-imagecheck-figure{border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-imagecheck-input:checked~.form-imagecheck-figure{border-color:var(--tblr-primary)}.form-imagecheck-figure:before{position:absolute;top:.25rem;left:.25rem;z-index:1;display:block;width:1rem;height:1rem;color:#fff;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:var(--tblr-bg-forms);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius);transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-figure:before{transition:none}}.form-imagecheck-input:checked~.form-imagecheck-figure:before{background-color:var(--tblr-primary);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e");background-repeat:repeat;background-position:center;background-size:1rem;border-color:var(--tblr-border-color-translucent)}.form-imagecheck-input[type=radio]~.form-imagecheck-figure:before{border-radius:50%}.form-imagecheck-input[type=radio]:checked~.form-imagecheck-figure:before{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-imagecheck-image{max-width:100%;display:block;opacity:.64;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-image{transition:none}}.form-imagecheck-image:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.form-imagecheck-image:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.form-imagecheck-input:checked~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck:hover .form-imagecheck-image{opacity:1}.form-imagecheck-caption{padding:.25rem;font-size:.765625rem;color:var(--tblr-muted);text-align:center;transition:color .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-caption{transition:none}}.form-imagecheck-input:checked~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck:hover .form-imagecheck-caption{color:#1d273b}.form-selectgroup{display:inline-flex;margin:0 -.5rem -.5rem 0;flex-wrap:wrap}.form-selectgroup .form-selectgroup-item{margin:0 .5rem .5rem 0}.form-selectgroup-vertical{flex-direction:column}.form-selectgroup-item{display:block;position:relative}.form-selectgroup-input{position:absolute;top:0;left:0;z-index:-1;opacity:0}.form-selectgroup-label{position:relative;display:block;min-width:calc(1.4285714286em + .875rem + 2px);margin:0;padding:.4375rem .75rem;font-size:.875rem;line-height:1.4285714286;color:var(--tblr-muted);background:var(--tblr-bg-forms);text-align:center;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:3px;transition:border-color .3s,background .3s,color .3s}@media (prefers-reduced-motion:reduce){.form-selectgroup-label{transition:none}}.form-selectgroup-label .icon:only-child{margin:0 -.25rem}.form-selectgroup-label:hover{color:var(--tblr-body-color)}.form-selectgroup-check{display:inline-block;width:1rem;height:1rem;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);vertical-align:middle}.form-selectgroup-input[type=checkbox]+.form-selectgroup-label .form-selectgroup-check{border-radius:var(--tblr-border-radius)}.form-selectgroup-input[type=radio]+.form-selectgroup-label .form-selectgroup-check{border-radius:50%}.form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-check{background-color:var(--tblr-primary);background-repeat:repeat;background-position:center;background-size:1rem;border-color:var(--tblr-border-color-translucent)}.form-selectgroup-input[type=checkbox]:checked+.form-selectgroup-label .form-selectgroup-check{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-selectgroup-input[type=radio]:checked+.form-selectgroup-label .form-selectgroup-check{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-selectgroup-check-floated{position:absolute;top:.4375rem;right:.4375rem}.form-selectgroup-input:checked+.form-selectgroup-label{z-index:1;color:var(--tblr-primary);background:rgba(var(--tblr-primary-rgb),.04);border-color:var(--tblr-primary)}.form-selectgroup-input:focus+.form-selectgroup-label{z-index:2;color:var(--tblr-primary);border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(32,107,196,.25)}.form-selectgroup-boxes .form-selectgroup-label{text-align:left;padding:1.5rem 1rem;color:inherit}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label{color:inherit}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-title{color:var(--tblr-primary)}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-label-content{opacity:1}.form-selectgroup-pills{flex-wrap:wrap;align-items:flex-start}.form-selectgroup-pills .form-selectgroup-item{flex-grow:0}.form-selectgroup-pills .form-selectgroup-label{border-radius:50px}.form-control-color::-webkit-color-swatch{border:none}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none}.form-control::-webkit-file-upload-button{background-color:var(--tblr-btn-color,#f8fafc)}.form-control::file-selector-button{background-color:var(--tblr-btn-color,#f8fafc)}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--tblr-btn-color,#eceeef)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--tblr-btn-color,#eceeef)}.form-check{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.form-check.form-check-highlight .form-check-input:not(:checked)~.form-check-label{color:var(--tblr-muted)}.form-check .form-check-label-off{color:var(--tblr-muted)}.form-check .form-check-input:checked~.form-check-label-off{display:none}.form-check .form-check-input:not(:checked)~.form-check-label-on{display:none}.form-check-input{background-size:1rem;margin-top:.125rem}.form-switch .form-check-input{transition:background-color .3s,background-position .3s}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-check-label{display:block}.form-check-label.required:after{content:"*";margin-left:.25rem;color:#d63939}.form-check-description{display:block;color:var(--tblr-muted);font-size:.75rem;margin-top:.25rem}.form-check-single{margin:0}.form-check-single .form-check-input{margin:0}.form-switch .form-check-input{height:1.125rem;margin-top:.0625rem}.form-switch-lg{padding-left:3.5rem;min-height:1.5rem}.form-switch-lg .form-check-input{height:1.5rem;width:2.75rem;background-size:1.5rem;margin-left:-3.5rem}.form-switch-lg .form-check-label{padding-top:.125rem}.form-check-input:checked{border:none}.form-control.is-invalid-lite,.form-control.is-valid-lite,.form-select.is-invalid-lite,.form-select.is-valid-lite{border-color:var(--tblr-border-color)!important}.legend{--tblr-legend-size:0.75em;display:inline-block;background:var(--tblr-border-color);width:var(--tblr-legend-size);height:var(--tblr-legend-size);border-radius:var(--tblr-border-radius-sm)}.list-group{margin-left:0;margin-right:0}.list-group-header{background:var(--tblr-light);padding:.5rem 1.5rem;font-size:.75rem;font-weight:var(--tblr-font-weight-medium);line-height:1;text-transform:uppercase;color:var(--tblr-muted);border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.list-group-flush>.list-group-header:last-child{border-bottom-width:0}.list-group-item{background-color:inherit}.list-group-item.active{background-color:rgba(var(--tblr-muted-rgb),.04);border-left-color:#206bc4;border-left-width:2px}.list-group-item:active,.list-group-item:focus,.list-group-item:hover{background-color:rgba(var(--tblr-muted-rgb),.04)}.list-group-item.disabled,.list-group-item:disabled{color:#6c7a91;background-color:rgba(var(--tblr-muted-rgb),.04)}.list-bordered .list-item{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);margin-top:-1px}.list-bordered .list-item:first-child{border-top:none}.list-group-hoverable .list-group-item-actions{opacity:0;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.list-group-hoverable .list-group-item-actions{transition:none}}.list-group-hoverable .list-group-item-actions.show,.list-group-hoverable .list-group-item:hover .list-group-item-actions{opacity:1}.list-timeline{position:relative;padding:0;margin:0;list-style:none}.list-timeline>li{position:relative;margin-bottom:1.5rem}.list-timeline>li:last-child{margin-bottom:0}.list-timeline-time{float:right;margin-left:1rem;color:var(--tblr-muted)}.list-timeline-icon{position:absolute;top:0;left:0;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;color:#fff;text-align:center;background:var(--tblr-muted);border-radius:100rem}.list-timeline-icon .icon{width:1rem;height:1rem;font-size:1rem}.list-timeline-title{margin:0;font-weight:var(--tblr-font-weight-bold)}.list-timeline-content{margin-left:3.5rem}@media screen and (min-width:768px){.list-timeline:not(.list-timeline-simple):before{position:absolute;top:0;bottom:0;left:calc(7.5rem + 2px);z-index:1;display:block;width:2px;content:"";background-color:var(--tblr-border-color)}.list-timeline:not(.list-timeline-simple)>li{z-index:2;min-height:40px}.list-timeline:not(.list-timeline-simple) .list-timeline-time{position:absolute;top:.5rem;left:0;width:5.5rem;margin:0;text-align:right}.list-timeline:not(.list-timeline-simple) .list-timeline-icon{top:0;left:6.5rem}.list-timeline:not(.list-timeline-simple) .list-timeline-content{padding:.625rem 0 0 10rem;margin:0}}.list-group-transparent{--tblr-list-group-border-radius:0;margin:0 -1.5rem}.list-group-transparent .list-group-item{background:0 0;border:0}.list-group-transparent .list-group-item .icon{color:var(--tblr-muted)}.list-group-transparent .list-group-item.active{font-weight:var(--tblr-font-weight-bold);color:inherit;background:var(--tblr-active-bg)}.list-group-transparent .list-group-item.active .icon{color:inherit}.list-separated-item{padding:1rem 0}.list-separated-item:first-child{padding-top:0}.list-separated-item:last-child{padding-bottom:0}.list-separated-item+.list-separated-item{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.list-inline-item:not(:last-child){margin-right:auto;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.list-inline-dots .list-inline-item+.list-inline-item:before{content:" · ";-webkit-margin-end:.5rem;margin-inline-end:.5rem}.loader{position:relative;display:block;width:2.5rem;height:2.5rem;color:#206bc4;vertical-align:middle}.loader:after{position:absolute;top:0;left:0;width:100%;height:100%;content:"";border:1px var(--tblr-border-style);border-color:transparent;border-top-color:currentColor;border-left-color:currentColor;border-radius:100rem;animation:rotate-360 .6s linear;animation-iteration-count:infinite}.dimmer{position:relative}.dimmer .loader{position:absolute;top:50%;right:0;left:0;display:none;margin:0 auto;transform:translateY(-50%)}.dimmer.active .loader{display:block}.dimmer.active .dimmer-content{pointer-events:none;opacity:.1}@keyframes animated-dots{0%{transform:translateX(-100%)}}.animated-dots{display:inline-block;overflow:hidden;vertical-align:bottom}.animated-dots:after{display:inline-block;content:"...";animation:animated-dots 1.2s steps(4,jump-none) infinite}.modal-content .btn-close{position:absolute;top:0;right:0;width:3.5rem;height:3.5rem;margin:0;padding:0;z-index:10}.modal-body::-webkit-scrollbar{width:.5rem;height:.5rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){.modal-body::-webkit-scrollbar{-webkit-transition:none;transition:none}}.modal-body::-webkit-scrollbar-thumb{border-radius:5px;background:rgba(var(--tblr-body-color-rgb),.16)}.modal-body::-webkit-scrollbar-track{background:rgba(var(--tblr-body-color-rgb),.06)}.modal-body:hover::-webkit-scrollbar-thumb{background:rgba(var(--tblr-body-color-rgb),.32)}.modal-body::-webkit-scrollbar-corner{background:0 0}.modal-body .modal-title{margin-bottom:1rem}.modal-body+.modal-body{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.modal-status{position:absolute;top:0;left:0;right:0;height:2px;background:var(--tblr-muted);border-radius:var(--tblr-border-radius-lg) var(--tblr-border-radius-lg) 0 0}.modal-header{align-items:center;min-height:3.5rem;background:#fff;padding:0 3.5rem 0 1.5rem}.modal-title{font-size:1rem;font-weight:var(--tblr-font-weight-medium);line-height:1.4285714286}.modal-footer{padding-top:0;padding-bottom:.75rem}.modal-blur{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.modal-full-width{max-width:none;margin:0 .5rem}.nav-vertical,.nav-vertical .nav{flex-direction:column;flex-wrap:nowrap}.nav-vertical .nav{margin-left:1.25rem;border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);padding-left:.5rem}.nav-vertical .nav-item.show .nav-link,.nav-vertical .nav-link.active{font-weight:var(--tblr-font-weight-bold)}.nav-vertical.nav-pills{margin:0 -.75rem}.nav-bordered{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.nav-bordered .nav-item+.nav-item{margin-left:1.25rem}.nav-bordered .nav-link{padding-left:0;padding-right:0;margin:0 0 -var(--tblr-border-width);border:0;border-bottom:2px var(--tblr-border-style) transparent;color:var(--tblr-muted)}.nav-bordered .nav-item.show .nav-link,.nav-bordered .nav-link.active{color:var(--tblr-primary);border-color:var(--tblr-primary)}.nav-link{display:flex;transition:color .3s;align-items:center}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link-toggle{margin-left:auto;padding:0 .25rem;transition:transform .3s}@media (prefers-reduced-motion:reduce){.nav-link-toggle{transition:none}}.nav-link-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-45deg)}.nav-link-toggle:after{margin:0}.nav-link[aria-expanded=true] .nav-link-toggle{transform:rotate(180deg)}.nav-link-icon{width:1.25rem;height:1.25rem;margin-right:.5rem;color:var(--tblr-icon-color)}.nav-link-icon svg{display:block;height:100%}.nav-fill .nav-item .nav-link{justify-content:center}.stars{display:inline-flex;color:#9ba9be;font-size:.75rem}.stars .star:not(:first-child){margin-left:.25rem}.pagination{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.page-link{min-width:1.75rem;border-radius:var(--tblr-border-radius)}.page-item{text-align:center}.page-item:not(.active) .page-link:hover{background:0 0}.page-item.page-next,.page-item.page-prev{flex:0 0 50%;text-align:left}.page-item.page-next{margin-left:auto;text-align:right}.page-item-subtitle{margin-bottom:2px;font-size:12px;color:var(--tblr-muted);text-transform:uppercase}.page-item.disabled .page-item-subtitle{color:var(--tblr-disabled-color)}.page-item-title{font-size:1rem;font-weight:var(--tblr-font-weight-normal);color:#1d273b}.page-link:hover .page-item-title{color:var(--tblr-primary)}.page-item.disabled .page-item-title{color:var(--tblr-disabled-color)}@keyframes progress-indeterminate{0%{right:100%;left:-35%}100%,60%{right:-90%;left:100%}}.progress{position:relative;width:100%;line-height:.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.progress::-webkit-progress-bar{background:var(--tblr-progress-bg)}.progress::-webkit-progress-value{background-color:var(--tblr-primary)}.progress::-moz-progress-bar{background-color:var(--tblr-primary)}.progress::-ms-fill{background-color:var(--tblr-primary);border:none}.progress-sm{height:.25rem}.progress-bar{height:100%}.progress-bar-indeterminate:after,.progress-bar-indeterminate:before{position:absolute;top:0;bottom:0;left:0;content:"";background-color:inherit;will-change:left,right}.progress-bar-indeterminate:before{animation:progress-indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}.progress-separated .progress-bar{box-shadow:0 0 0 2px var(--tblr-card-bg,var(--tblr-bg-surface))}.progressbg{position:relative;padding:.25rem .5rem;display:flex}.progressbg-text{position:relative;z-index:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progressbg-progress{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;height:100%;background:0 0;pointer-events:none}.progressbg-value{font-weight:var(--tblr-font-weight-medium);margin-left:auto;padding-left:2rem}.ribbon{--tblr-ribbon-margin:0.25rem;--tblr-ribbon-border-radius:var(--tblr-border-radius);position:absolute;top:.75rem;right:calc(-1 * var(--tblr-ribbon-margin));z-index:1;padding:.25rem .75rem;font-size:.625rem;font-weight:var(--tblr-font-weight-bold);line-height:1;color:#fff;text-align:center;text-transform:uppercase;background:var(--tblr-primary);border-color:var(--tblr-primary);border-radius:var(--tblr-ribbon-border-radius) 0 var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius);display:inline-flex;align-items:center;justify-content:center;min-height:2rem;min-width:2rem}.ribbon:before{position:absolute;right:0;bottom:100%;width:0;height:0;content:"";filter:brightness(70%);border:calc(var(--tblr-ribbon-margin) * .5) var(--tblr-border-style);border-color:inherit;border-top-color:transparent;border-right-color:transparent}.ribbon.bg-blue{border-color:var(--tblr-blue)}.ribbon.bg-blue-lt{border-color:rgba(var(--tblr-blue-rgb),.1)!important}.ribbon.bg-azure{border-color:var(--tblr-azure)}.ribbon.bg-azure-lt{border-color:rgba(var(--tblr-azure-rgb),.1)!important}.ribbon.bg-indigo{border-color:var(--tblr-indigo)}.ribbon.bg-indigo-lt{border-color:rgba(var(--tblr-indigo-rgb),.1)!important}.ribbon.bg-purple{border-color:var(--tblr-purple)}.ribbon.bg-purple-lt{border-color:rgba(var(--tblr-purple-rgb),.1)!important}.ribbon.bg-pink{border-color:var(--tblr-pink)}.ribbon.bg-pink-lt{border-color:rgba(var(--tblr-pink-rgb),.1)!important}.ribbon.bg-red{border-color:var(--tblr-red)}.ribbon.bg-red-lt{border-color:rgba(var(--tblr-red-rgb),.1)!important}.ribbon.bg-orange{border-color:var(--tblr-orange)}.ribbon.bg-orange-lt{border-color:rgba(var(--tblr-orange-rgb),.1)!important}.ribbon.bg-yellow{border-color:var(--tblr-yellow)}.ribbon.bg-yellow-lt{border-color:rgba(var(--tblr-yellow-rgb),.1)!important}.ribbon.bg-lime{border-color:var(--tblr-lime)}.ribbon.bg-lime-lt{border-color:rgba(var(--tblr-lime-rgb),.1)!important}.ribbon.bg-green{border-color:var(--tblr-green)}.ribbon.bg-green-lt{border-color:rgba(var(--tblr-green-rgb),.1)!important}.ribbon.bg-teal{border-color:var(--tblr-teal)}.ribbon.bg-teal-lt{border-color:rgba(var(--tblr-teal-rgb),.1)!important}.ribbon.bg-cyan{border-color:var(--tblr-cyan)}.ribbon.bg-cyan-lt{border-color:rgba(var(--tblr-cyan-rgb),.1)!important}.ribbon .icon{width:1.25rem;height:1.25rem;font-size:1.25rem}.ribbon-top{top:calc(-1 * var(--tblr-ribbon-margin));right:.75rem;width:2rem;padding:.5rem 0;border-radius:0 var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius)}.ribbon-top:before{top:0;right:100%;bottom:auto;border-color:inherit;border-top-color:transparent;border-left-color:transparent}.ribbon-top.ribbon-start{right:auto;left:.75rem}.ribbon-top.ribbon-start:before{top:0;right:100%;left:auto}.ribbon-start{right:auto;left:calc(-1 * var(--tblr-ribbon-margin))}.ribbon-start:before{top:auto;bottom:100%;left:0;border-color:inherit;border-top-color:transparent;border-left-color:transparent}.ribbon-bottom{top:auto;bottom:.75rem}.ribbon-bookmark{padding-left:.25rem;border-radius:0 0 var(--tblr-ribbon-border-radius) 0}.ribbon-bookmark:after{position:absolute;top:0;right:100%;display:block;width:0;height:0;content:"";border:1rem var(--tblr-border-style);border-color:inherit;border-right-width:0;border-left-color:transparent;border-left-width:.5rem}.ribbon-bookmark.ribbon-left{padding-right:.5rem}.ribbon-bookmark.ribbon-left:after{right:auto;left:100%;border-right-color:transparent;border-right-width:.5rem;border-left-width:0}.ribbon-bookmark.ribbon-top{padding-right:0;padding-bottom:.25rem;padding-left:0;border-radius:0 var(--tblr-ribbon-border-radius) 0 0}.ribbon-bookmark.ribbon-top:after{top:100%;right:0;left:0;border-color:inherit;border-width:1rem;border-top-width:0;border-bottom-color:transparent;border-bottom-width:.5rem}.markdown{line-height:1.7142857143}.markdown>:first-child{margin-top:0}.markdown>:last-child,.markdown>:last-child .highlight{margin-bottom:0}@media (min-width:768px){.markdown>.hr,.markdown>hr{margin-top:3em;margin-bottom:3em}}.markdown>.h1,.markdown>.h2,.markdown>.h3,.markdown>.h4,.markdown>.h5,.markdown>.h6,.markdown>h1,.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{font-weight:var(--tblr-font-weight-bold)}.markdown>blockquote{font-size:1rem;margin:1.5rem 0;padding:.5rem 1.5rem}.markdown>img{border-radius:var(--tblr-border-radius)}.placeholder:not(.btn):not([class*=bg-]){background-color:currentColor!important}.placeholder:not(.avatar):not([class*=card-img-]){border-radius:var(--tblr-border-radius)}.steps{--tblr-steps-color:var(--tblr-primary);--tblr-steps-inactive-color:var(--tblr-border-color);--tblr-steps-dot-size:.5rem;--tblr-steps-border-width:2px;display:flex;flex-wrap:nowrap;width:100%;padding:0;margin:0;list-style:none}.steps-blue{--tblr-steps-color:var(--tblr-blue)}.steps-azure{--tblr-steps-color:var(--tblr-azure)}.steps-indigo{--tblr-steps-color:var(--tblr-indigo)}.steps-purple{--tblr-steps-color:var(--tblr-purple)}.steps-pink{--tblr-steps-color:var(--tblr-pink)}.steps-red{--tblr-steps-color:var(--tblr-red)}.steps-orange{--tblr-steps-color:var(--tblr-orange)}.steps-yellow{--tblr-steps-color:var(--tblr-yellow)}.steps-lime{--tblr-steps-color:var(--tblr-lime)}.steps-green{--tblr-steps-color:var(--tblr-green)}.steps-teal{--tblr-steps-color:var(--tblr-teal)}.steps-cyan{--tblr-steps-color:var(--tblr-cyan)}.step-item{position:relative;flex:1 1 0;min-height:1rem;margin-top:0;color:inherit;text-align:center;cursor:default;padding-top:calc(var(--tblr-steps-dot-size))}a.step-item{cursor:pointer}a.step-item:hover{color:inherit}.step-item:after,.step-item:before{background:var(--tblr-steps-color)}.step-item:not(:last-child):after{position:absolute;left:50%;width:100%;content:"";transform:translateY(-50%)}.step-item:after{top:calc(var(--tblr-steps-dot-size) * .5);height:var(--tblr-steps-border-width)}.step-item:before{content:"";position:absolute;top:0;left:50%;z-index:1;box-sizing:content-box;display:flex;align-items:center;justify-content:center;border-radius:100rem;transform:translateX(-50%);color:var(--tblr-white);width:var(--tblr-steps-dot-size);height:var(--tblr-steps-dot-size)}.step-item.active{font-weight:var(--tblr-font-weight-bold)}.step-item.active:after{background:var(--tblr-steps-inactive-color)}.step-item.active~.step-item{color:var(--tblr-disabled-color)}.step-item.active~.step-item:after,.step-item.active~.step-item:before{background:var(--tblr-steps-inactive-color)}.steps-counter{--tblr-steps-dot-size:1.5rem;counter-reset:steps}.steps-counter .step-item{counter-increment:steps}.steps-counter .step-item:before{content:counter(steps)}.steps-vertical{--tblr-steps-dot-offset:6px;flex-direction:column}.steps-vertical.steps-counter{--tblr-steps-dot-offset:-2px}.steps-vertical .step-item{text-align:left;padding-top:0;padding-left:calc(var(--tblr-steps-dot-size) + 1rem);min-height:auto}.steps-vertical .step-item:not(:first-child){margin-top:1rem}.steps-vertical .step-item:before{top:var(--tblr-steps-dot-offset);left:0;transform:translate(0,0)}.steps-vertical .step-item:not(:last-child):after{position:absolute;content:"";transform:translateX(-50%);top:var(--tblr-steps-dot-offset);left:calc(var(--tblr-steps-dot-size) * .5);width:var(--tblr-steps-border-width);height:calc(100% + 1rem)}@keyframes status-pulsate-main{40%{transform:scale(1.25,1.25)}60%{transform:scale(1.25,1.25)}}@keyframes status-pulsate-secondary{10%{transform:scale(1,1)}30%{transform:scale(3,3)}80%{transform:scale(3,3)}100%{transform:scale(1,1)}}@keyframes status-pulsate-tertiary{25%{transform:scale(1,1)}80%{transform:scale(3,3);opacity:0}100%{transform:scale(3,3);opacity:0}}.status{--tblr-status-height:1.5rem;--tblr-status-color:#616876;--tblr-status-color-rgb:97,104,118;display:inline-flex;align-items:center;height:var(--tblr-status-height);padding:.25rem .75rem;gap:.5rem;color:var(--tblr-status-color);background:rgba(var(--tblr-status-color-rgb),.1);font-size:.875rem;text-transform:none;letter-spacing:normal;border-radius:100rem;font-weight:var(--tblr-font-weight-medium);line-height:1;margin:0}.status .status-dot{background:var(--tblr-status-color)}.status .icon{font-size:1.25rem}.status-lite{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)!important;background:0 0!important;color:var(--tblr-body-text)!important}.status-primary{--tblr-status-color:#206bc4;--tblr-status-color-rgb:32,107,196}.status-secondary{--tblr-status-color:#616876;--tblr-status-color-rgb:97,104,118}.status-success{--tblr-status-color:#2fb344;--tblr-status-color-rgb:47,179,68}.status-info{--tblr-status-color:#4299e1;--tblr-status-color-rgb:66,153,225}.status-warning{--tblr-status-color:#f76707;--tblr-status-color-rgb:247,103,7}.status-danger{--tblr-status-color:#d63939;--tblr-status-color-rgb:214,57,57}.status-light{--tblr-status-color:#f8fafc;--tblr-status-color-rgb:248,250,252}.status-dark{--tblr-status-color:#1d273b;--tblr-status-color-rgb:29,39,59}.status-muted{--tblr-status-color:#616876;--tblr-status-color-rgb:97,104,118}.status-blue{--tblr-status-color:#206bc4;--tblr-status-color-rgb:32,107,196}.status-azure{--tblr-status-color:#4299e1;--tblr-status-color-rgb:66,153,225}.status-indigo{--tblr-status-color:#4263eb;--tblr-status-color-rgb:66,99,235}.status-purple{--tblr-status-color:#ae3ec9;--tblr-status-color-rgb:174,62,201}.status-pink{--tblr-status-color:#d6336c;--tblr-status-color-rgb:214,51,108}.status-red{--tblr-status-color:#d63939;--tblr-status-color-rgb:214,57,57}.status-orange{--tblr-status-color:#f76707;--tblr-status-color-rgb:247,103,7}.status-yellow{--tblr-status-color:#f59f00;--tblr-status-color-rgb:245,159,0}.status-lime{--tblr-status-color:#74b816;--tblr-status-color-rgb:116,184,22}.status-green{--tblr-status-color:#2fb344;--tblr-status-color-rgb:47,179,68}.status-teal{--tblr-status-color:#0ca678;--tblr-status-color-rgb:12,166,120}.status-cyan{--tblr-status-color:#17a2b8;--tblr-status-color-rgb:23,162,184}.status-facebook{--tblr-status-color:#1877F2;--tblr-status-color-rgb:24,119,242}.status-twitter{--tblr-status-color:#1da1f2;--tblr-status-color-rgb:29,161,242}.status-linkedin{--tblr-status-color:#0a66c2;--tblr-status-color-rgb:10,102,194}.status-google{--tblr-status-color:#dc4e41;--tblr-status-color-rgb:220,78,65}.status-youtube{--tblr-status-color:#ff0000;--tblr-status-color-rgb:255,0,0}.status-vimeo{--tblr-status-color:#1ab7ea;--tblr-status-color-rgb:26,183,234}.status-dribbble{--tblr-status-color:#ea4c89;--tblr-status-color-rgb:234,76,137}.status-github{--tblr-status-color:#181717;--tblr-status-color-rgb:24,23,23}.status-instagram{--tblr-status-color:#e4405f;--tblr-status-color-rgb:228,64,95}.status-pinterest{--tblr-status-color:#bd081c;--tblr-status-color-rgb:189,8,28}.status-vk{--tblr-status-color:#6383a8;--tblr-status-color-rgb:99,131,168}.status-rss{--tblr-status-color:#ffa500;--tblr-status-color-rgb:255,165,0}.status-flickr{--tblr-status-color:#0063dc;--tblr-status-color-rgb:0,99,220}.status-bitbucket{--tblr-status-color:#0052cc;--tblr-status-color-rgb:0,82,204}.status-tabler{--tblr-status-color:#206bc4;--tblr-status-color-rgb:32,107,196}.status-dot{--tblr-status-dot-color:var(--tblr-status-color, #616876);--tblr-status-size:0.5rem;position:relative;display:inline-block;width:var(--tblr-status-size);height:var(--tblr-status-size);background:var(--tblr-status-dot-color);border-radius:100rem}.status-dot-animated:before{content:"";position:absolute;inset:0;z-index:0;background:inherit;border-radius:inherit;opacity:.6;animation:1s linear 2s backwards infinite status-pulsate-tertiary}.status-indicator{--tblr-status-indicator-size:2.5rem;--tblr-status-indicator-color:var(--tblr-status-color, #616876);display:block;position:relative;width:var(--tblr-status-indicator-size);height:var(--tblr-status-indicator-size)}.status-indicator-circle{--tblr-status-circle-size:.75rem;position:absolute;left:50%;top:50%;margin:calc(var(--tblr-status-circle-size)/ -2) 0 0 calc(var(--tblr-status-circle-size)/ -2);width:var(--tblr-status-circle-size);height:var(--tblr-status-circle-size);border-radius:100rem;background:var(--tblr-status-color)}.status-indicator-circle:nth-child(1){z-index:3}.status-indicator-circle:nth-child(2){z-index:2;opacity:.1}.status-indicator-circle:nth-child(3){z-index:1;opacity:.3}.status-indicator-animated .status-indicator-circle:nth-child(1){animation:2s linear 1s infinite backwards status-pulsate-main}.status-indicator-animated .status-indicator-circle:nth-child(2){animation:2s linear 1s infinite backwards status-pulsate-secondary}.status-indicator-animated .status-indicator-circle:nth-child(3){animation:2s linear 1s infinite backwards status-pulsate-tertiary}.switch-icon{display:inline-block;line-height:1;border:0;padding:0;background:0 0;width:1.25rem;height:1.25rem;vertical-align:bottom;position:relative;cursor:pointer}.switch-icon.disabled{pointer-events:none;opacity:.4}.switch-icon:focus{outline:0}.switch-icon svg{display:block;width:100%;height:100%}.switch-icon .switch-icon-a,.switch-icon .switch-icon-b{display:block;width:100%;height:100%}.switch-icon .switch-icon-a{opacity:1}.switch-icon .switch-icon-b{position:absolute;top:0;left:0;opacity:0}.switch-icon.active .switch-icon-a{opacity:0}.switch-icon.active .switch-icon-b{opacity:1}.switch-icon-fade .switch-icon-a,.switch-icon-fade .switch-icon-b{transition:opacity .5s}@media (prefers-reduced-motion:reduce){.switch-icon-fade .switch-icon-a,.switch-icon-fade .switch-icon-b{transition:none}}.switch-icon-scale .switch-icon-a,.switch-icon-scale .switch-icon-b{transition:opacity .5s,transform 0s .5s}@media (prefers-reduced-motion:reduce){.switch-icon-scale .switch-icon-a,.switch-icon-scale .switch-icon-b{transition:none}}.switch-icon-scale .switch-icon-b{transform:scale(1.5)}.switch-icon-scale.active .switch-icon-a,.switch-icon-scale.active .switch-icon-b{transition:opacity 0s,transform .5s}@media (prefers-reduced-motion:reduce){.switch-icon-scale.active .switch-icon-a,.switch-icon-scale.active .switch-icon-b{transition:none}}.switch-icon-scale.active .switch-icon-b{transform:scale(1)}.switch-icon-flip{perspective:10em}.switch-icon-flip .switch-icon-a,.switch-icon-flip .switch-icon-b{-webkit-backface-visibility:hidden;backface-visibility:hidden;transform-style:preserve-3d;transition:opacity 0s .2s,transform .4s ease-in-out}@media (prefers-reduced-motion:reduce){.switch-icon-flip .switch-icon-a,.switch-icon-flip .switch-icon-b{transition:none}}.switch-icon-flip .switch-icon-a{opacity:1;transform:rotateY(0)}.switch-icon-flip .switch-icon-b{opacity:1;transform:rotateY(-180deg)}.switch-icon-flip.active .switch-icon-a{opacity:1;transform:rotateY(180deg)}.switch-icon-flip.active .switch-icon-b{opacity:1;transform:rotateY(0)}.switch-icon-slide-down,.switch-icon-slide-left,.switch-icon-slide-right,.switch-icon-slide-up{overflow:hidden}.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b{transition:opacity .3s,transform .3s}@media (prefers-reduced-motion:reduce){.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b{transition:none}}.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-up .switch-icon-a{transform:translateY(0)}.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-up .switch-icon-b{transform:translateY(100%)}.switch-icon-slide-down.active .switch-icon-a,.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-up.active .switch-icon-a{transform:translateY(-100%)}.switch-icon-slide-down.active .switch-icon-b,.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-up.active .switch-icon-b{transform:translateY(0)}.switch-icon-slide-left .switch-icon-a{transform:translateX(0)}.switch-icon-slide-left .switch-icon-b{transform:translateX(100%)}.switch-icon-slide-left.active .switch-icon-a{transform:translateX(-100%)}.switch-icon-slide-left.active .switch-icon-b{transform:translateX(0)}.switch-icon-slide-right .switch-icon-a{transform:translateX(0)}.switch-icon-slide-right .switch-icon-b{transform:translateX(-100%)}.switch-icon-slide-right.active .switch-icon-a{transform:translateX(100%)}.switch-icon-slide-right.active .switch-icon-b{transform:translateX(0)}.switch-icon-slide-down .switch-icon-a{transform:translateY(0)}.switch-icon-slide-down .switch-icon-b{transform:translateY(-100%)}.switch-icon-slide-down.active .switch-icon-a{transform:translateY(100%)}.switch-icon-slide-down.active .switch-icon-b{transform:translateY(0)}@media not print{.theme-dark .table-primary{--tblr-table-color:#f8fafc;--tblr-table-bg:#134076;--tblr-table-border-color:#2a5383;--tblr-table-striped-bg:#1e497d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#2a5383;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#244e80;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark .table-secondary{--tblr-table-color:#f8fafc;--tblr-table-bg:#3a3e47;--tblr-table-border-color:#4d5159;--tblr-table-striped-bg:#444750;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#4d5159;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#484c55;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark .table-success{--tblr-table-color:#f8fafc;--tblr-table-bg:#1c6b29;--tblr-table-border-color:#32793e;--tblr-table-striped-bg:#277234;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#32793e;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#2d7639;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark .table-info{--tblr-table-color:#f8fafc;--tblr-table-bg:#285c87;--tblr-table-border-color:#3d6c93;--tblr-table-striped-bg:#32648d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#3d6c93;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#386890;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark .table-warning{--tblr-table-color:#f8fafc;--tblr-table-bg:#943e04;--tblr-table-border-color:#9e511d;--tblr-table-striped-bg:#994710;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#9e511d;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#9c4c17;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark .table-danger{--tblr-table-color:#f8fafc;--tblr-table-bg:#802222;--tblr-table-border-color:#8c3838;--tblr-table-striped-bg:#862d2d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#8c3838;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#893232;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}}@media not print{@media (prefers-color-scheme:dark){.theme-dark-auto .table-primary{--tblr-table-color:#f8fafc;--tblr-table-bg:#134076;--tblr-table-border-color:#2a5383;--tblr-table-striped-bg:#1e497d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#2a5383;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#244e80;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark-auto .table-secondary{--tblr-table-color:#f8fafc;--tblr-table-bg:#3a3e47;--tblr-table-border-color:#4d5159;--tblr-table-striped-bg:#444750;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#4d5159;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#484c55;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark-auto .table-success{--tblr-table-color:#f8fafc;--tblr-table-bg:#1c6b29;--tblr-table-border-color:#32793e;--tblr-table-striped-bg:#277234;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#32793e;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#2d7639;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark-auto .table-info{--tblr-table-color:#f8fafc;--tblr-table-bg:#285c87;--tblr-table-border-color:#3d6c93;--tblr-table-striped-bg:#32648d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#3d6c93;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#386890;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark-auto .table-warning{--tblr-table-color:#f8fafc;--tblr-table-bg:#943e04;--tblr-table-border-color:#9e511d;--tblr-table-striped-bg:#994710;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#9e511d;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#9c4c17;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.theme-dark-auto .table-danger{--tblr-table-color:#f8fafc;--tblr-table-bg:#802222;--tblr-table-border-color:#8c3838;--tblr-table-striped-bg:#862d2d;--tblr-table-striped-color:#f8fafc;--tblr-table-active-bg:#8c3838;--tblr-table-active-color:#f8fafc;--tblr-table-hover-bg:#893232;--tblr-table-hover-color:#f8fafc;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}}}.markdown>table thead th,.table thead th{color:var(--tblr-muted);background:var(--tblr-gray-50);font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);padding-top:.5rem;padding-bottom:.5rem;white-space:nowrap}@media print{.markdown>table thead th,.table thead th{background:0 0}}.table-responsive .markdown>table,.table-responsive .table{margin-bottom:0}.table-responsive+.card-footer{border-top:0}.table-transparent thead th{background:0 0}.table-nowrap>:not(caption)>*>*{white-space:nowrap}.table-vcenter>:not(caption)>*>*{vertical-align:middle}.table-center>:not(caption)>*>*{text-align:center}.td-truncate{max-width:1px;width:100%}.table-mobile{display:block}.table-mobile thead{display:none}.table-mobile tbody,.table-mobile tr{display:flex;flex-direction:column}.table-mobile td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile .btn{display:block}@media (max-width:575.98px){.table-mobile-sm{display:block}.table-mobile-sm thead{display:none}.table-mobile-sm tbody,.table-mobile-sm tr{display:flex;flex-direction:column}.table-mobile-sm td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile-sm td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile-sm tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-sm .btn{display:block}}@media (max-width:767.98px){.table-mobile-md{display:block}.table-mobile-md thead{display:none}.table-mobile-md tbody,.table-mobile-md tr{display:flex;flex-direction:column}.table-mobile-md td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile-md td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile-md tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-md .btn{display:block}}@media (max-width:991.98px){.table-mobile-lg{display:block}.table-mobile-lg thead{display:none}.table-mobile-lg tbody,.table-mobile-lg tr{display:flex;flex-direction:column}.table-mobile-lg td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile-lg td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile-lg tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-lg .btn{display:block}}@media (max-width:1199.98px){.table-mobile-xl{display:block}.table-mobile-xl thead{display:none}.table-mobile-xl tbody,.table-mobile-xl tr{display:flex;flex-direction:column}.table-mobile-xl td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile-xl td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile-xl tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-xl .btn{display:block}}@media (max-width:1399.98px){.table-mobile-xxl{display:block}.table-mobile-xxl thead{display:none}.table-mobile-xxl tbody,.table-mobile-xxl tr{display:flex;flex-direction:column}.table-mobile-xxl td{display:block;padding:.75rem .75rem!important;border:none;color:#1d273b!important}.table-mobile-xxl td[data-label]:before{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);content:attr(data-label);display:block}.table-mobile-xxl tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-xxl .btn{display:block}}.table-sort{font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;border:0;background:inherit;display:block;width:100%;text-align:inherit;transition:color .3s;margin:-.5rem -.75rem;padding:.5rem .75rem}@media (prefers-reduced-motion:reduce){.table-sort{transition:none}}.table-sort.asc,.table-sort.desc,.table-sort:hover{color:#1d273b}.table-sort.asc:after,.table-sort.desc:after,.table-sort:after{content:"";display:inline-flex;width:1rem;height:1rem;vertical-align:bottom;background:url("data:image/svg+xml,") no-repeat center;opacity:.2}.table-sort.asc:after{background:url("data:image/svg+xml,") no-repeat center;opacity:1}.table-sort.desc:after{background:url("data:image/svg+xml,") no-repeat center;opacity:1}.table-borderless thead th{background:0 0}.toast{background:#fff;border:1px var(--tblr-border-style) var(--tblr-border-color-translucent);box-shadow:rgba(29,39,59,.04) 0 2px 4px 0}.toast .toast-header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.toast button[data-bs-dismiss=toast]{outline:0}.toast-primary{--tblr-toast-color:#206bc4}.toast-secondary{--tblr-toast-color:#616876}.toast-success{--tblr-toast-color:#2fb344}.toast-info{--tblr-toast-color:#4299e1}.toast-warning{--tblr-toast-color:#f76707}.toast-danger{--tblr-toast-color:#d63939}.toast-light{--tblr-toast-color:#f8fafc}.toast-dark{--tblr-toast-color:#1d273b}.toast-muted{--tblr-toast-color:#616876}.toast-blue{--tblr-toast-color:#206bc4}.toast-azure{--tblr-toast-color:#4299e1}.toast-indigo{--tblr-toast-color:#4263eb}.toast-purple{--tblr-toast-color:#ae3ec9}.toast-pink{--tblr-toast-color:#d6336c}.toast-red{--tblr-toast-color:#d63939}.toast-orange{--tblr-toast-color:#f76707}.toast-yellow{--tblr-toast-color:#f59f00}.toast-lime{--tblr-toast-color:#74b816}.toast-green{--tblr-toast-color:#2fb344}.toast-teal{--tblr-toast-color:#0ca678}.toast-cyan{--tblr-toast-color:#17a2b8}.toast-facebook{--tblr-toast-color:#1877F2}.toast-twitter{--tblr-toast-color:#1da1f2}.toast-linkedin{--tblr-toast-color:#0a66c2}.toast-google{--tblr-toast-color:#dc4e41}.toast-youtube{--tblr-toast-color:#ff0000}.toast-vimeo{--tblr-toast-color:#1ab7ea}.toast-dribbble{--tblr-toast-color:#ea4c89}.toast-github{--tblr-toast-color:#181717}.toast-instagram{--tblr-toast-color:#e4405f}.toast-pinterest{--tblr-toast-color:#bd081c}.toast-vk{--tblr-toast-color:#6383a8}.toast-rss{--tblr-toast-color:#ffa500}.toast-flickr{--tblr-toast-color:#0063dc}.toast-bitbucket{--tblr-toast-color:#0052cc}.toast-tabler{--tblr-toast-color:#206bc4}.toolbar{display:flex;flex-wrap:nowrap;flex-shrink:0;margin:0 -.5rem}.toolbar>*{margin:0 .5rem}.tracking{--tblr-tracking-height:1.5rem;--tblr-tracking-gap-width:0.125rem;--tblr-tracking-block-border-radius:var(--tblr-border-radius);display:flex;gap:var(--tblr-tracking-gap-width)}.tracking-squares{--tblr-tracking-block-border-radius:var(--tblr-border-radius-sm)}.tracking-squares .tracking-block{height:auto}.tracking-squares .tracking-block:before{content:"";display:block;padding-top:100%}.tracking-block{flex:1;border-radius:var(--tblr-tracking-block-border-radius);height:var(--tblr-tracking-height);min-width:.25rem;background:var(--tblr-border-color)}.hr-text{display:flex;align-items:center;margin:2rem 0;font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted);height:1px}.hr-text:after,.hr-text:before{flex:1 1 auto;height:1px;background-color:var(--tblr-border-color)}.hr-text:before{content:"";margin-right:.5rem}.hr-text:after{content:"";margin-left:.5rem}.hr-text>:first-child{padding-right:.5rem;padding-left:0;color:var(--tblr-muted)}.hr-text.hr-text-left:before{content:none}.hr-text.hr-text-left>:first-child{padding-right:.5rem;padding-left:.5rem}.hr-text.hr-text-right:before{content:""}.hr-text.hr-text-right:after{content:none}.hr-text.hr-text-right>:first-child{padding-right:0;padding-left:.5rem}.card>.hr-text{margin:0}.hr-text-spaceless{margin:-.5rem 0}.lead{line-height:1.4}a{-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}.h1 a,.h2 a,.h3 a,.h4 a,.h5 a,.h6 a,h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:inherit}.h1 a:hover,.h2 a:hover,.h3 a:hover,.h4 a:hover,.h5 a:hover,.h6 a:hover,h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color:inherit}.h1,h1{font-size:var(--tblr-font-size-h1);line-height:var(--tblr-line-height-h1)}.h2,h2{font-size:var(--tblr-font-size-h2);line-height:var(--tblr-line-height-h2)}.h3,h3{font-size:var(--tblr-font-size-h3);line-height:var(--tblr-line-height-h3)}.h4,h4{font-size:var(--tblr-font-size-h4);line-height:var(--tblr-line-height-h4)}.h5,h5{font-size:var(--tblr-font-size-h5);line-height:var(--tblr-line-height-h5)}.h6,h6{font-size:var(--tblr-font-size-h6);line-height:var(--tblr-line-height-h6)}.strong,b,strong{font-weight:var(--tblr-font-weight-bold)}blockquote{padding-left:1rem;border-left:2px var(--tblr-border-style) var(--tblr-border-color)}blockquote p{margin-bottom:1rem}blockquote cite{display:block;text-align:right}blockquote cite:before{content:"— "}ol,ul{padding-left:1.5rem}.hr,hr{margin:2rem 0}dl dd:last-child{margin-bottom:0}pre{padding:1rem;background:var(--tblr-bg-surface-dark);color:var(--tblr-light);border-radius:var(--tblr-border-radius)}pre code{background:0 0}code{background:var(--tblr-code-bg);padding:2px 4px;border-radius:var(--tblr-border-radius)}kbd{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);display:inline-block;box-sizing:border-box;max-width:100%;font-size:var(--tblr-font-size-h5);font-weight:var(--tblr-font-weight-medium);line-height:1;vertical-align:baseline}img{max-width:100%}.list-unstyled{margin-left:0}::-moz-selection{background-color:rgba(var(--tblr-primary-rgb),.16)}::selection{background-color:rgba(var(--tblr-primary-rgb),.16)}[class*=" link-"].disabled,[class^=link-].disabled{color:var(--tblr-disabled-color);pointer-events:none}.subheader{font-size:.625rem;font-weight:var(--tblr-font-weight-bold);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-muted)}.chart{display:block;min-height:10rem}.chart text{font-family:inherit}.chart-sm{height:2.5rem}.chart-lg{height:15rem}.chart-square{height:5.75rem}.chart-sparkline{position:relative;width:4rem;height:2.5rem;line-height:1;min-height:0!important}.chart-sparkline-sm{height:1.5rem}.chart-sparkline-square{width:2.5rem}.chart-sparkline-wide{width:6rem}.chart-sparkline-label{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;font-size:.625rem}.chart-sparkline-label .icon{width:1rem;height:1rem;font-size:1rem}.offcanvas-header{border-bottom:var(--tblr-border-width) var(--tblr-border-style) rgba(97,104,118,.16)}.offcanvas-footer{padding:1.5rem 1.5rem}.offcanvas-title{font-size:1rem;font-weight:var(--tblr-font-weight-medium);line-height:1.5rem}.offcanvas-narrow{width:20rem}.bg-white-overlay{color:#fff;background-color:rgba(248,250,252,.24)}.bg-dark-overlay{color:#fff;background-color:rgba(29,39,59,.24)}.bg-cover{background-repeat:no-repeat;background-size:cover;background-position:center}.bg-primary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-primary-rgb),var(--tblr-bg-opacity))!important}.bg-primary-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-primary-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-primary-rgb),var(--tblr-bg-opacity))!important}.bg-secondary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-secondary-rgb),var(--tblr-bg-opacity))!important}.bg-secondary-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-secondary-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-secondary-rgb),var(--tblr-bg-opacity))!important}.bg-success{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-success-rgb),var(--tblr-bg-opacity))!important}.bg-success-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-success-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-success-rgb),var(--tblr-bg-opacity))!important}.bg-info{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-info-rgb),var(--tblr-bg-opacity))!important}.bg-info-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-info-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-info-rgb),var(--tblr-bg-opacity))!important}.bg-warning{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-warning-rgb),var(--tblr-bg-opacity))!important}.bg-warning-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-warning-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-warning-rgb),var(--tblr-bg-opacity))!important}.bg-danger{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-danger-rgb),var(--tblr-bg-opacity))!important}.bg-danger-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-danger-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-danger-rgb),var(--tblr-bg-opacity))!important}.bg-light{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-light-rgb),var(--tblr-bg-opacity))!important}.bg-light-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-light-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-light-rgb),var(--tblr-bg-opacity))!important}.bg-dark{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-dark-rgb),var(--tblr-bg-opacity))!important}.bg-dark-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-dark-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-dark-rgb),var(--tblr-bg-opacity))!important}.bg-muted{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-muted-rgb),var(--tblr-bg-opacity))!important}.bg-muted-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-muted-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-muted-rgb),var(--tblr-bg-opacity))!important}.bg-blue{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-blue-rgb),var(--tblr-bg-opacity))!important}.bg-blue-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-blue-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-blue-rgb),var(--tblr-bg-opacity))!important}.bg-azure{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-azure-rgb),var(--tblr-bg-opacity))!important}.bg-azure-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-azure-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-azure-rgb),var(--tblr-bg-opacity))!important}.bg-indigo{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-indigo-rgb),var(--tblr-bg-opacity))!important}.bg-indigo-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-indigo-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-indigo-rgb),var(--tblr-bg-opacity))!important}.bg-purple{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-purple-rgb),var(--tblr-bg-opacity))!important}.bg-purple-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-purple-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-purple-rgb),var(--tblr-bg-opacity))!important}.bg-pink{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-pink-rgb),var(--tblr-bg-opacity))!important}.bg-pink-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-pink-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-pink-rgb),var(--tblr-bg-opacity))!important}.bg-red{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-red-rgb),var(--tblr-bg-opacity))!important}.bg-red-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-red-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-red-rgb),var(--tblr-bg-opacity))!important}.bg-orange{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-orange-rgb),var(--tblr-bg-opacity))!important}.bg-orange-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-orange-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-orange-rgb),var(--tblr-bg-opacity))!important}.bg-yellow{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-yellow-rgb),var(--tblr-bg-opacity))!important}.bg-yellow-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-yellow-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-yellow-rgb),var(--tblr-bg-opacity))!important}.bg-lime{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-lime-rgb),var(--tblr-bg-opacity))!important}.bg-lime-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-lime-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-lime-rgb),var(--tblr-bg-opacity))!important}.bg-green{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-green-rgb),var(--tblr-bg-opacity))!important}.bg-green-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-green-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-green-rgb),var(--tblr-bg-opacity))!important}.bg-teal{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-teal-rgb),var(--tblr-bg-opacity))!important}.bg-teal-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-teal-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-teal-rgb),var(--tblr-bg-opacity))!important}.bg-cyan{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-cyan-rgb),var(--tblr-bg-opacity))!important}.bg-cyan-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-cyan-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-cyan-rgb),var(--tblr-bg-opacity))!important}.bg-facebook{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-facebook-rgb),var(--tblr-bg-opacity))!important}.bg-facebook-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-facebook-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-facebook-rgb),var(--tblr-bg-opacity))!important}.bg-twitter{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-twitter-rgb),var(--tblr-bg-opacity))!important}.bg-twitter-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-twitter-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-twitter-rgb),var(--tblr-bg-opacity))!important}.bg-linkedin{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-linkedin-rgb),var(--tblr-bg-opacity))!important}.bg-linkedin-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-linkedin-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-linkedin-rgb),var(--tblr-bg-opacity))!important}.bg-google{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-google-rgb),var(--tblr-bg-opacity))!important}.bg-google-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-google-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-google-rgb),var(--tblr-bg-opacity))!important}.bg-youtube{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-youtube-rgb),var(--tblr-bg-opacity))!important}.bg-youtube-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-youtube-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-youtube-rgb),var(--tblr-bg-opacity))!important}.bg-vimeo{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-vimeo-rgb),var(--tblr-bg-opacity))!important}.bg-vimeo-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-vimeo-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-vimeo-rgb),var(--tblr-bg-opacity))!important}.bg-dribbble{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-dribbble-rgb),var(--tblr-bg-opacity))!important}.bg-dribbble-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-dribbble-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-dribbble-rgb),var(--tblr-bg-opacity))!important}.bg-github{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-github-rgb),var(--tblr-bg-opacity))!important}.bg-github-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-github-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-github-rgb),var(--tblr-bg-opacity))!important}.bg-instagram{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-instagram-rgb),var(--tblr-bg-opacity))!important}.bg-instagram-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-instagram-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-instagram-rgb),var(--tblr-bg-opacity))!important}.bg-pinterest{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-pinterest-rgb),var(--tblr-bg-opacity))!important}.bg-pinterest-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-pinterest-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-pinterest-rgb),var(--tblr-bg-opacity))!important}.bg-vk{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-vk-rgb),var(--tblr-bg-opacity))!important}.bg-vk-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-vk-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-vk-rgb),var(--tblr-bg-opacity))!important}.bg-rss{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-rss-rgb),var(--tblr-bg-opacity))!important}.bg-rss-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-rss-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-rss-rgb),var(--tblr-bg-opacity))!important}.bg-flickr{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-flickr-rgb),var(--tblr-bg-opacity))!important}.bg-flickr-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-flickr-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-flickr-rgb),var(--tblr-bg-opacity))!important}.bg-bitbucket{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-bg-opacity))!important}.bg-bitbucket-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-bitbucket-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-bg-opacity))!important}.bg-tabler{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-tabler-rgb),var(--tblr-bg-opacity))!important}.bg-tabler-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-tabler-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-tabler-rgb),var(--tblr-bg-opacity))!important}.bg-white{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-white-rgb),var(--tblr-bg-opacity))!important}.bg-white-lt{--tblr-bg-opacity:.1;--tblr-text-opacity:1;color:rgba(var(--tblr-white-rgb,var(--tblr-text-opacity)))!important;background-color:rgba(var(--tblr-white-rgb),var(--tblr-bg-opacity))!important}.text-primary{--tblr-text-opacity:1;color:rgba(var(--tblr-primary-rgb),var(--tblr-text-opacity))!important}.text-primary-fg{color:var(--tblr-primary-fg)!important}.text-secondary{--tblr-text-opacity:1;color:rgba(var(--tblr-secondary-rgb),var(--tblr-text-opacity))!important}.text-secondary-fg{color:var(--tblr-secondary-fg)!important}.text-success{--tblr-text-opacity:1;color:rgba(var(--tblr-success-rgb),var(--tblr-text-opacity))!important}.text-success-fg{color:var(--tblr-success-fg)!important}.text-info{--tblr-text-opacity:1;color:rgba(var(--tblr-info-rgb),var(--tblr-text-opacity))!important}.text-info-fg{color:var(--tblr-info-fg)!important}.text-warning{--tblr-text-opacity:1;color:rgba(var(--tblr-warning-rgb),var(--tblr-text-opacity))!important}.text-warning-fg{color:var(--tblr-warning-fg)!important}.text-danger{--tblr-text-opacity:1;color:rgba(var(--tblr-danger-rgb),var(--tblr-text-opacity))!important}.text-danger-fg{color:var(--tblr-danger-fg)!important}.text-light{--tblr-text-opacity:1;color:rgba(var(--tblr-light-rgb),var(--tblr-text-opacity))!important}.text-light-fg{color:var(--tblr-light-fg)!important}.text-dark{--tblr-text-opacity:1;color:rgba(var(--tblr-dark-rgb),var(--tblr-text-opacity))!important}.text-dark-fg{color:var(--tblr-dark-fg)!important}.text-muted{--tblr-text-opacity:1;color:rgba(var(--tblr-muted-rgb),var(--tblr-text-opacity))!important}.text-muted-fg{color:var(--tblr-muted-fg)!important}.text-blue{--tblr-text-opacity:1;color:rgba(var(--tblr-blue-rgb),var(--tblr-text-opacity))!important}.text-blue-fg{color:var(--tblr-blue-fg)!important}.text-azure{--tblr-text-opacity:1;color:rgba(var(--tblr-azure-rgb),var(--tblr-text-opacity))!important}.text-azure-fg{color:var(--tblr-azure-fg)!important}.text-indigo{--tblr-text-opacity:1;color:rgba(var(--tblr-indigo-rgb),var(--tblr-text-opacity))!important}.text-indigo-fg{color:var(--tblr-indigo-fg)!important}.text-purple{--tblr-text-opacity:1;color:rgba(var(--tblr-purple-rgb),var(--tblr-text-opacity))!important}.text-purple-fg{color:var(--tblr-purple-fg)!important}.text-pink{--tblr-text-opacity:1;color:rgba(var(--tblr-pink-rgb),var(--tblr-text-opacity))!important}.text-pink-fg{color:var(--tblr-pink-fg)!important}.text-red{--tblr-text-opacity:1;color:rgba(var(--tblr-red-rgb),var(--tblr-text-opacity))!important}.text-red-fg{color:var(--tblr-red-fg)!important}.text-orange{--tblr-text-opacity:1;color:rgba(var(--tblr-orange-rgb),var(--tblr-text-opacity))!important}.text-orange-fg{color:var(--tblr-orange-fg)!important}.text-yellow{--tblr-text-opacity:1;color:rgba(var(--tblr-yellow-rgb),var(--tblr-text-opacity))!important}.text-yellow-fg{color:var(--tblr-yellow-fg)!important}.text-lime{--tblr-text-opacity:1;color:rgba(var(--tblr-lime-rgb),var(--tblr-text-opacity))!important}.text-lime-fg{color:var(--tblr-lime-fg)!important}.text-green{--tblr-text-opacity:1;color:rgba(var(--tblr-green-rgb),var(--tblr-text-opacity))!important}.text-green-fg{color:var(--tblr-green-fg)!important}.text-teal{--tblr-text-opacity:1;color:rgba(var(--tblr-teal-rgb),var(--tblr-text-opacity))!important}.text-teal-fg{color:var(--tblr-teal-fg)!important}.text-cyan{--tblr-text-opacity:1;color:rgba(var(--tblr-cyan-rgb),var(--tblr-text-opacity))!important}.text-cyan-fg{color:var(--tblr-cyan-fg)!important}.text-facebook{--tblr-text-opacity:1;color:rgba(var(--tblr-facebook-rgb),var(--tblr-text-opacity))!important}.text-facebook-fg{color:var(--tblr-facebook-fg)!important}.text-twitter{--tblr-text-opacity:1;color:rgba(var(--tblr-twitter-rgb),var(--tblr-text-opacity))!important}.text-twitter-fg{color:var(--tblr-twitter-fg)!important}.text-linkedin{--tblr-text-opacity:1;color:rgba(var(--tblr-linkedin-rgb),var(--tblr-text-opacity))!important}.text-linkedin-fg{color:var(--tblr-linkedin-fg)!important}.text-google{--tblr-text-opacity:1;color:rgba(var(--tblr-google-rgb),var(--tblr-text-opacity))!important}.text-google-fg{color:var(--tblr-google-fg)!important}.text-youtube{--tblr-text-opacity:1;color:rgba(var(--tblr-youtube-rgb),var(--tblr-text-opacity))!important}.text-youtube-fg{color:var(--tblr-youtube-fg)!important}.text-vimeo{--tblr-text-opacity:1;color:rgba(var(--tblr-vimeo-rgb),var(--tblr-text-opacity))!important}.text-vimeo-fg{color:var(--tblr-vimeo-fg)!important}.text-dribbble{--tblr-text-opacity:1;color:rgba(var(--tblr-dribbble-rgb),var(--tblr-text-opacity))!important}.text-dribbble-fg{color:var(--tblr-dribbble-fg)!important}.text-github{--tblr-text-opacity:1;color:rgba(var(--tblr-github-rgb),var(--tblr-text-opacity))!important}.text-github-fg{color:var(--tblr-github-fg)!important}.text-instagram{--tblr-text-opacity:1;color:rgba(var(--tblr-instagram-rgb),var(--tblr-text-opacity))!important}.text-instagram-fg{color:var(--tblr-instagram-fg)!important}.text-pinterest{--tblr-text-opacity:1;color:rgba(var(--tblr-pinterest-rgb),var(--tblr-text-opacity))!important}.text-pinterest-fg{color:var(--tblr-pinterest-fg)!important}.text-vk{--tblr-text-opacity:1;color:rgba(var(--tblr-vk-rgb),var(--tblr-text-opacity))!important}.text-vk-fg{color:var(--tblr-vk-fg)!important}.text-rss{--tblr-text-opacity:1;color:rgba(var(--tblr-rss-rgb),var(--tblr-text-opacity))!important}.text-rss-fg{color:var(--tblr-rss-fg)!important}.text-flickr{--tblr-text-opacity:1;color:rgba(var(--tblr-flickr-rgb),var(--tblr-text-opacity))!important}.text-flickr-fg{color:var(--tblr-flickr-fg)!important}.text-bitbucket{--tblr-text-opacity:1;color:rgba(var(--tblr-bitbucket-rgb),var(--tblr-text-opacity))!important}.text-bitbucket-fg{color:var(--tblr-bitbucket-fg)!important}.text-tabler{--tblr-text-opacity:1;color:rgba(var(--tblr-tabler-rgb),var(--tblr-text-opacity))!important}.text-tabler-fg{color:var(--tblr-tabler-fg)!important}.bg-gray-50{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-50-rgb),var(--tblr-bg-opacity))!important}.text-gray-50-fg{color:#1d273b!important}.bg-gray-100{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-100-rgb),var(--tblr-bg-opacity))!important}.text-gray-100-fg{color:#1d273b!important}.bg-gray-200{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-200-rgb),var(--tblr-bg-opacity))!important}.text-gray-200-fg{color:#1d273b!important}.bg-gray-300{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-300-rgb),var(--tblr-bg-opacity))!important}.text-gray-300-fg{color:#1d273b!important}.bg-gray-400{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-400-rgb),var(--tblr-bg-opacity))!important}.text-gray-400-fg{color:#f8fafc!important}.bg-gray-500{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-500-rgb),var(--tblr-bg-opacity))!important}.text-gray-500-fg{color:#f8fafc!important}.bg-gray-600{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-600-rgb),var(--tblr-bg-opacity))!important}.text-gray-600-fg{color:#f8fafc!important}.bg-gray-700{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-700-rgb),var(--tblr-bg-opacity))!important}.text-gray-700-fg{color:#f8fafc!important}.bg-gray-800{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-800-rgb),var(--tblr-bg-opacity))!important}.text-gray-800-fg{color:#f8fafc!important}.bg-gray-900{--tblr-bg-opacity:.1;background-color:rgba(var(--tblr-gray-900-rgb),var(--tblr-bg-opacity))!important}.text-gray-900-fg{color:#f8fafc!important}.scrollable{overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.scrollable.hover{overflow-y:hidden}.scrollable.hover>*{margin-top:-1px}.scrollable.hover:active,.scrollable.hover:focus,.scrollable.hover:hover{overflow:visible;overflow-y:auto}.touch .scrollable{overflow-y:auto!important}.scroll-x,.scroll-y{overflow:hidden;-webkit-overflow-scrolling:touch}.scroll-y{overflow-y:auto}.scroll-x{overflow-x:auto}.no-scroll{overflow:hidden}.w-0{width:0!important}.h-0{height:0!important}.w-1{width:.25rem!important}.h-1{height:.25rem!important}.w-2{width:.5rem!important}.h-2{height:.5rem!important}.w-3{width:1rem!important}.h-3{height:1rem!important}.w-4{width:2rem!important}.h-4{height:2rem!important}.w-5{width:4rem!important}.h-5{height:4rem!important}.w-auto{width:auto!important}.h-auto{height:auto!important}.w-px{width:1px!important}.h-px{height:1px!important}.w-full{width:100%!important}.h-full{height:100%!important}.opacity-0{opacity:0!important}.opacity-5{opacity:.05!important}.opacity-10{opacity:.1!important}.opacity-15{opacity:.15!important}.opacity-20{opacity:.2!important}.opacity-25{opacity:.25!important}.opacity-30{opacity:.3!important}.opacity-35{opacity:.35!important}.opacity-40{opacity:.4!important}.opacity-45{opacity:.45!important}.opacity-50{opacity:.5!important}.opacity-55{opacity:.55!important}.opacity-60{opacity:.6!important}.opacity-65{opacity:.65!important}.opacity-70{opacity:.7!important}.opacity-75{opacity:.75!important}.opacity-80{opacity:.8!important}.opacity-85{opacity:.85!important}.opacity-90{opacity:.9!important}.opacity-95{opacity:.95!important}.opacity-100{opacity:1!important}.hover-shadow-sm:hover{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.hover-shadow:hover{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.hover-shadow-lg:hover{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.hover-shadow-none:hover{box-shadow:none!important}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto} \ No newline at end of file diff --git a/web/static/favicon.ico b/web/static/favicon.ico new file mode 100644 index 0000000..7090611 Binary files /dev/null and b/web/static/favicon.ico differ diff --git a/web/static/img/115.jpg b/web/static/img/115.jpg new file mode 100644 index 0000000..87457ac Binary files /dev/null and b/web/static/img/115.jpg differ diff --git a/web/static/img/aria2.png b/web/static/img/aria2.png new file mode 100644 index 0000000..1de5e3d Binary files /dev/null and b/web/static/img/aria2.png differ diff --git a/web/static/img/bark.webp b/web/static/img/bark.webp new file mode 100644 index 0000000..0d6bb2f Binary files /dev/null and b/web/static/img/bark.webp differ diff --git a/web/static/img/bug_fixing.svg b/web/static/img/bug_fixing.svg new file mode 100644 index 0000000..51a0ea8 --- /dev/null +++ b/web/static/img/bug_fixing.svg @@ -0,0 +1 @@ +bug fixing \ No newline at end of file diff --git a/web/static/img/chanify.png b/web/static/img/chanify.png new file mode 100644 index 0000000..71934c7 Binary files /dev/null and b/web/static/img/chanify.png differ diff --git a/web/static/img/chinesesubfinder.png b/web/static/img/chinesesubfinder.png new file mode 100644 index 0000000..5ba3ea5 Binary files /dev/null and b/web/static/img/chinesesubfinder.png differ diff --git a/web/static/img/emby.png b/web/static/img/emby.png new file mode 100644 index 0000000..6720f33 Binary files /dev/null and b/web/static/img/emby.png differ diff --git a/web/static/img/filetree/application.png b/web/static/img/filetree/application.png new file mode 100644 index 0000000..1dee9e3 Binary files /dev/null and b/web/static/img/filetree/application.png differ diff --git a/web/static/img/filetree/code.png b/web/static/img/filetree/code.png new file mode 100644 index 0000000..0c76bd1 Binary files /dev/null and b/web/static/img/filetree/code.png differ diff --git a/web/static/img/filetree/css.png b/web/static/img/filetree/css.png new file mode 100644 index 0000000..f907e44 Binary files /dev/null and b/web/static/img/filetree/css.png differ diff --git a/web/static/img/filetree/db.png b/web/static/img/filetree/db.png new file mode 100644 index 0000000..bddba1f Binary files /dev/null and b/web/static/img/filetree/db.png differ diff --git a/web/static/img/filetree/directory-lock.png b/web/static/img/filetree/directory-lock.png new file mode 100644 index 0000000..211697d Binary files /dev/null and b/web/static/img/filetree/directory-lock.png differ diff --git a/web/static/img/filetree/directory.png b/web/static/img/filetree/directory.png new file mode 100644 index 0000000..784e8fa Binary files /dev/null and b/web/static/img/filetree/directory.png differ diff --git a/web/static/img/filetree/doc.png b/web/static/img/filetree/doc.png new file mode 100644 index 0000000..ae8ecbf Binary files /dev/null and b/web/static/img/filetree/doc.png differ diff --git a/web/static/img/filetree/file-lock.png b/web/static/img/filetree/file-lock.png new file mode 100644 index 0000000..e909cf8 Binary files /dev/null and b/web/static/img/filetree/file-lock.png differ diff --git a/web/static/img/filetree/file.png b/web/static/img/filetree/file.png new file mode 100644 index 0000000..8b8b1ca Binary files /dev/null and b/web/static/img/filetree/file.png differ diff --git a/web/static/img/filetree/film.png b/web/static/img/filetree/film.png new file mode 100644 index 0000000..b0ce7bb Binary files /dev/null and b/web/static/img/filetree/film.png differ diff --git a/web/static/img/filetree/flash.png b/web/static/img/filetree/flash.png new file mode 100644 index 0000000..5769120 Binary files /dev/null and b/web/static/img/filetree/flash.png differ diff --git a/web/static/img/filetree/folder_open.png b/web/static/img/filetree/folder_open.png new file mode 100644 index 0000000..4e35483 Binary files /dev/null and b/web/static/img/filetree/folder_open.png differ diff --git a/web/static/img/filetree/html.png b/web/static/img/filetree/html.png new file mode 100644 index 0000000..6ed2490 Binary files /dev/null and b/web/static/img/filetree/html.png differ diff --git a/web/static/img/filetree/java.png b/web/static/img/filetree/java.png new file mode 100644 index 0000000..b7bfcd1 Binary files /dev/null and b/web/static/img/filetree/java.png differ diff --git a/web/static/img/filetree/linux.png b/web/static/img/filetree/linux.png new file mode 100644 index 0000000..52699bf Binary files /dev/null and b/web/static/img/filetree/linux.png differ diff --git a/web/static/img/filetree/music.png b/web/static/img/filetree/music.png new file mode 100644 index 0000000..a8b3ede Binary files /dev/null and b/web/static/img/filetree/music.png differ diff --git a/web/static/img/filetree/pdf.png b/web/static/img/filetree/pdf.png new file mode 100644 index 0000000..8f8095e Binary files /dev/null and b/web/static/img/filetree/pdf.png differ diff --git a/web/static/img/filetree/php.png b/web/static/img/filetree/php.png new file mode 100644 index 0000000..7868a25 Binary files /dev/null and b/web/static/img/filetree/php.png differ diff --git a/web/static/img/filetree/picture.png b/web/static/img/filetree/picture.png new file mode 100644 index 0000000..4a158fe Binary files /dev/null and b/web/static/img/filetree/picture.png differ diff --git a/web/static/img/filetree/ppt.png b/web/static/img/filetree/ppt.png new file mode 100644 index 0000000..c4eff03 Binary files /dev/null and b/web/static/img/filetree/ppt.png differ diff --git a/web/static/img/filetree/psd.png b/web/static/img/filetree/psd.png new file mode 100644 index 0000000..73c5b3f Binary files /dev/null and b/web/static/img/filetree/psd.png differ diff --git a/web/static/img/filetree/ruby.png b/web/static/img/filetree/ruby.png new file mode 100644 index 0000000..f59b7c4 Binary files /dev/null and b/web/static/img/filetree/ruby.png differ diff --git a/web/static/img/filetree/script.png b/web/static/img/filetree/script.png new file mode 100644 index 0000000..63fe6ce Binary files /dev/null and b/web/static/img/filetree/script.png differ diff --git a/web/static/img/filetree/spinner.gif b/web/static/img/filetree/spinner.gif new file mode 100644 index 0000000..85b99d4 Binary files /dev/null and b/web/static/img/filetree/spinner.gif differ diff --git a/web/static/img/filetree/txt.png b/web/static/img/filetree/txt.png new file mode 100644 index 0000000..813f712 Binary files /dev/null and b/web/static/img/filetree/txt.png differ diff --git a/web/static/img/filetree/xls.png b/web/static/img/filetree/xls.png new file mode 100644 index 0000000..b977d7e Binary files /dev/null and b/web/static/img/filetree/xls.png differ diff --git a/web/static/img/filetree/zip.png b/web/static/img/filetree/zip.png new file mode 100644 index 0000000..fd4bbcc Binary files /dev/null and b/web/static/img/filetree/zip.png differ diff --git a/web/static/img/gotify.png b/web/static/img/gotify.png new file mode 100644 index 0000000..5d29004 Binary files /dev/null and b/web/static/img/gotify.png differ diff --git a/web/static/img/icon-imdb.png b/web/static/img/icon-imdb.png new file mode 100644 index 0000000..f50d380 Binary files /dev/null and b/web/static/img/icon-imdb.png differ diff --git a/web/static/img/icons/1024.png b/web/static/img/icons/1024.png new file mode 100644 index 0000000..6b174ca Binary files /dev/null and b/web/static/img/icons/1024.png differ diff --git a/web/static/img/icons/128.png b/web/static/img/icons/128.png new file mode 100644 index 0000000..790f2c8 Binary files /dev/null and b/web/static/img/icons/128.png differ diff --git a/web/static/img/icons/144.png b/web/static/img/icons/144.png new file mode 100644 index 0000000..1abdb14 Binary files /dev/null and b/web/static/img/icons/144.png differ diff --git a/web/static/img/icons/152.png b/web/static/img/icons/152.png new file mode 100644 index 0000000..fca6f4f Binary files /dev/null and b/web/static/img/icons/152.png differ diff --git a/web/static/img/icons/167.png b/web/static/img/icons/167.png new file mode 100644 index 0000000..ddf9ceb Binary files /dev/null and b/web/static/img/icons/167.png differ diff --git a/web/static/img/icons/172.png b/web/static/img/icons/172.png new file mode 100644 index 0000000..f36a9c2 Binary files /dev/null and b/web/static/img/icons/172.png differ diff --git a/web/static/img/icons/180.png b/web/static/img/icons/180.png new file mode 100644 index 0000000..c56c380 Binary files /dev/null and b/web/static/img/icons/180.png differ diff --git a/web/static/img/icons/196.png b/web/static/img/icons/196.png new file mode 100644 index 0000000..695d25a Binary files /dev/null and b/web/static/img/icons/196.png differ diff --git a/web/static/img/icons/196_ALT.png b/web/static/img/icons/196_ALT.png new file mode 100644 index 0000000..90079ad Binary files /dev/null and b/web/static/img/icons/196_ALT.png differ diff --git a/web/static/img/icons/216.png b/web/static/img/icons/216.png new file mode 100644 index 0000000..5b3ab9a Binary files /dev/null and b/web/static/img/icons/216.png differ diff --git a/web/static/img/icons/256.png b/web/static/img/icons/256.png new file mode 100644 index 0000000..28b579b Binary files /dev/null and b/web/static/img/icons/256.png differ diff --git a/web/static/img/icons/512.png b/web/static/img/icons/512.png new file mode 100644 index 0000000..01d65c7 Binary files /dev/null and b/web/static/img/icons/512.png differ diff --git a/web/static/img/icons/512_ALT.png b/web/static/img/icons/512_ALT.png new file mode 100644 index 0000000..55d501a Binary files /dev/null and b/web/static/img/icons/512_ALT.png differ diff --git a/web/static/img/indexer.jpg b/web/static/img/indexer.jpg new file mode 100644 index 0000000..92024a3 Binary files /dev/null and b/web/static/img/indexer.jpg differ diff --git a/web/static/img/indexer.png b/web/static/img/indexer.png new file mode 100644 index 0000000..aca1d6b Binary files /dev/null and b/web/static/img/indexer.png differ diff --git a/web/static/img/iyuu.png b/web/static/img/iyuu.png new file mode 100644 index 0000000..1904a7d Binary files /dev/null and b/web/static/img/iyuu.png differ diff --git a/web/static/img/jackett.png b/web/static/img/jackett.png new file mode 100644 index 0000000..6cd3bc3 Binary files /dev/null and b/web/static/img/jackett.png differ diff --git a/web/static/img/jellyfin.jpg b/web/static/img/jellyfin.jpg new file mode 100644 index 0000000..a8acb09 Binary files /dev/null and b/web/static/img/jellyfin.jpg differ diff --git a/web/static/img/jellyfin.png b/web/static/img/jellyfin.png new file mode 100644 index 0000000..a46021f Binary files /dev/null and b/web/static/img/jellyfin.png differ diff --git a/web/static/img/joyride.svg b/web/static/img/joyride.svg new file mode 100644 index 0000000..49c4fec --- /dev/null +++ b/web/static/img/joyride.svg @@ -0,0 +1 @@ +joyride \ No newline at end of file diff --git a/web/static/img/logo-16x16.png b/web/static/img/logo-16x16.png new file mode 100644 index 0000000..3c5bf49 Binary files /dev/null and b/web/static/img/logo-16x16.png differ diff --git a/web/static/img/logo-32x32.png b/web/static/img/logo-32x32.png new file mode 100644 index 0000000..c96bfa4 Binary files /dev/null and b/web/static/img/logo-32x32.png differ diff --git a/web/static/img/logo-black.png b/web/static/img/logo-black.png new file mode 100644 index 0000000..9e6ad0b Binary files /dev/null and b/web/static/img/logo-black.png differ diff --git a/web/static/img/logo-blue.png b/web/static/img/logo-blue.png new file mode 100644 index 0000000..7d25783 Binary files /dev/null and b/web/static/img/logo-blue.png differ diff --git a/web/static/img/logo-white.png b/web/static/img/logo-white.png new file mode 100644 index 0000000..aaaeb33 Binary files /dev/null and b/web/static/img/logo-white.png differ diff --git a/web/static/img/logo.png b/web/static/img/logo.png new file mode 100644 index 0000000..bc771fa Binary files /dev/null and b/web/static/img/logo.png differ diff --git a/web/static/img/medicine.svg b/web/static/img/medicine.svg new file mode 100644 index 0000000..9cf293e --- /dev/null +++ b/web/static/img/medicine.svg @@ -0,0 +1 @@ +medicine \ No newline at end of file diff --git a/web/static/img/mobile_application.svg b/web/static/img/mobile_application.svg new file mode 100644 index 0000000..35e80ff --- /dev/null +++ b/web/static/img/mobile_application.svg @@ -0,0 +1 @@ +Mobile_application \ No newline at end of file diff --git a/web/static/img/movie.jpg b/web/static/img/movie.jpg new file mode 100644 index 0000000..26e992e Binary files /dev/null and b/web/static/img/movie.jpg differ diff --git a/web/static/img/music.png b/web/static/img/music.png new file mode 100644 index 0000000..35dafe0 Binary files /dev/null and b/web/static/img/music.png differ diff --git a/web/static/img/no-image.png b/web/static/img/no-image.png new file mode 100644 index 0000000..bdae745 Binary files /dev/null and b/web/static/img/no-image.png differ diff --git a/web/static/img/opensubtitles.png b/web/static/img/opensubtitles.png new file mode 100644 index 0000000..94c39a3 Binary files /dev/null and b/web/static/img/opensubtitles.png differ diff --git a/web/static/img/person.png b/web/static/img/person.png new file mode 100644 index 0000000..6efad30 Binary files /dev/null and b/web/static/img/person.png differ diff --git a/web/static/img/pikpak.png b/web/static/img/pikpak.png new file mode 100644 index 0000000..a75fe16 Binary files /dev/null and b/web/static/img/pikpak.png differ diff --git a/web/static/img/plex.png b/web/static/img/plex.png new file mode 100644 index 0000000..b9f3665 Binary files /dev/null and b/web/static/img/plex.png differ diff --git a/web/static/img/posting_photo.svg b/web/static/img/posting_photo.svg new file mode 100644 index 0000000..47cb4fc --- /dev/null +++ b/web/static/img/posting_photo.svg @@ -0,0 +1 @@ +posting photo \ No newline at end of file diff --git a/web/static/img/printing_invoices.svg b/web/static/img/printing_invoices.svg new file mode 100644 index 0000000..3c7a0b6 --- /dev/null +++ b/web/static/img/printing_invoices.svg @@ -0,0 +1 @@ +printing invoices \ No newline at end of file diff --git a/web/static/img/prowlarr.png b/web/static/img/prowlarr.png new file mode 100644 index 0000000..d4bb74e Binary files /dev/null and b/web/static/img/prowlarr.png differ diff --git a/web/static/img/pt.jpg b/web/static/img/pt.jpg new file mode 100644 index 0000000..4e10824 Binary files /dev/null and b/web/static/img/pt.jpg differ diff --git a/web/static/img/pushdeer.png b/web/static/img/pushdeer.png new file mode 100644 index 0000000..af25f50 Binary files /dev/null and b/web/static/img/pushdeer.png differ diff --git a/web/static/img/pushplus.jpg b/web/static/img/pushplus.jpg new file mode 100644 index 0000000..5ab63ae Binary files /dev/null and b/web/static/img/pushplus.jpg differ diff --git a/web/static/img/qbittorrent.png b/web/static/img/qbittorrent.png new file mode 100644 index 0000000..44eb9d7 Binary files /dev/null and b/web/static/img/qbittorrent.png differ diff --git a/web/static/img/quitting_time.svg b/web/static/img/quitting_time.svg new file mode 100644 index 0000000..6209538 --- /dev/null +++ b/web/static/img/quitting_time.svg @@ -0,0 +1 @@ +quitting time \ No newline at end of file diff --git a/web/static/img/serverchan.png b/web/static/img/serverchan.png new file mode 100644 index 0000000..6c5f715 Binary files /dev/null and b/web/static/img/serverchan.png differ diff --git a/web/static/img/sign_in.svg b/web/static/img/sign_in.svg new file mode 100644 index 0000000..465016d --- /dev/null +++ b/web/static/img/sign_in.svg @@ -0,0 +1 @@ +sign_in \ No newline at end of file diff --git a/web/static/img/slack.png b/web/static/img/slack.png new file mode 100644 index 0000000..a6a743f Binary files /dev/null and b/web/static/img/slack.png differ diff --git a/web/static/img/splash/apple-splash-1125-2436.png b/web/static/img/splash/apple-splash-1125-2436.png new file mode 100644 index 0000000..e6d4962 Binary files /dev/null and b/web/static/img/splash/apple-splash-1125-2436.png differ diff --git a/web/static/img/splash/apple-splash-1136-640.png b/web/static/img/splash/apple-splash-1136-640.png new file mode 100644 index 0000000..31eb9bd Binary files /dev/null and b/web/static/img/splash/apple-splash-1136-640.png differ diff --git a/web/static/img/splash/apple-splash-1170-2532.png b/web/static/img/splash/apple-splash-1170-2532.png new file mode 100644 index 0000000..0d6efef Binary files /dev/null and b/web/static/img/splash/apple-splash-1170-2532.png differ diff --git a/web/static/img/splash/apple-splash-1242-2208.png b/web/static/img/splash/apple-splash-1242-2208.png new file mode 100644 index 0000000..3d049a6 Binary files /dev/null and b/web/static/img/splash/apple-splash-1242-2208.png differ diff --git a/web/static/img/splash/apple-splash-1242-2688.png b/web/static/img/splash/apple-splash-1242-2688.png new file mode 100644 index 0000000..ec8c7e9 Binary files /dev/null and b/web/static/img/splash/apple-splash-1242-2688.png differ diff --git a/web/static/img/splash/apple-splash-1284-2778.png b/web/static/img/splash/apple-splash-1284-2778.png new file mode 100644 index 0000000..92a65de Binary files /dev/null and b/web/static/img/splash/apple-splash-1284-2778.png differ diff --git a/web/static/img/splash/apple-splash-1334-750.png b/web/static/img/splash/apple-splash-1334-750.png new file mode 100644 index 0000000..7c188c2 Binary files /dev/null and b/web/static/img/splash/apple-splash-1334-750.png differ diff --git a/web/static/img/splash/apple-splash-1536-2048.png b/web/static/img/splash/apple-splash-1536-2048.png new file mode 100644 index 0000000..1587a65 Binary files /dev/null and b/web/static/img/splash/apple-splash-1536-2048.png differ diff --git a/web/static/img/splash/apple-splash-1620-2160.png b/web/static/img/splash/apple-splash-1620-2160.png new file mode 100644 index 0000000..1b15e50 Binary files /dev/null and b/web/static/img/splash/apple-splash-1620-2160.png differ diff --git a/web/static/img/splash/apple-splash-1668-2224.png b/web/static/img/splash/apple-splash-1668-2224.png new file mode 100644 index 0000000..5b736f2 Binary files /dev/null and b/web/static/img/splash/apple-splash-1668-2224.png differ diff --git a/web/static/img/splash/apple-splash-1668-2388.png b/web/static/img/splash/apple-splash-1668-2388.png new file mode 100644 index 0000000..5934789 Binary files /dev/null and b/web/static/img/splash/apple-splash-1668-2388.png differ diff --git a/web/static/img/splash/apple-splash-1792-828.png b/web/static/img/splash/apple-splash-1792-828.png new file mode 100644 index 0000000..1b7bc69 Binary files /dev/null and b/web/static/img/splash/apple-splash-1792-828.png differ diff --git a/web/static/img/splash/apple-splash-2048-1536.png b/web/static/img/splash/apple-splash-2048-1536.png new file mode 100644 index 0000000..5d7cfa6 Binary files /dev/null and b/web/static/img/splash/apple-splash-2048-1536.png differ diff --git a/web/static/img/splash/apple-splash-2048-2732.png b/web/static/img/splash/apple-splash-2048-2732.png new file mode 100644 index 0000000..b7e2e9d Binary files /dev/null and b/web/static/img/splash/apple-splash-2048-2732.png differ diff --git a/web/static/img/splash/apple-splash-2160-1620.png b/web/static/img/splash/apple-splash-2160-1620.png new file mode 100644 index 0000000..a484651 Binary files /dev/null and b/web/static/img/splash/apple-splash-2160-1620.png differ diff --git a/web/static/img/splash/apple-splash-2208-1242.png b/web/static/img/splash/apple-splash-2208-1242.png new file mode 100644 index 0000000..cf0310c Binary files /dev/null and b/web/static/img/splash/apple-splash-2208-1242.png differ diff --git a/web/static/img/splash/apple-splash-2224-1668.png b/web/static/img/splash/apple-splash-2224-1668.png new file mode 100644 index 0000000..0b5fd9d Binary files /dev/null and b/web/static/img/splash/apple-splash-2224-1668.png differ diff --git a/web/static/img/splash/apple-splash-2388-1668.png b/web/static/img/splash/apple-splash-2388-1668.png new file mode 100644 index 0000000..30a9508 Binary files /dev/null and b/web/static/img/splash/apple-splash-2388-1668.png differ diff --git a/web/static/img/splash/apple-splash-2436-1125.png b/web/static/img/splash/apple-splash-2436-1125.png new file mode 100644 index 0000000..3105315 Binary files /dev/null and b/web/static/img/splash/apple-splash-2436-1125.png differ diff --git a/web/static/img/splash/apple-splash-2532-1170.png b/web/static/img/splash/apple-splash-2532-1170.png new file mode 100644 index 0000000..126f9bb Binary files /dev/null and b/web/static/img/splash/apple-splash-2532-1170.png differ diff --git a/web/static/img/splash/apple-splash-2688-1242.png b/web/static/img/splash/apple-splash-2688-1242.png new file mode 100644 index 0000000..4b5961b Binary files /dev/null and b/web/static/img/splash/apple-splash-2688-1242.png differ diff --git a/web/static/img/splash/apple-splash-2732-2048.png b/web/static/img/splash/apple-splash-2732-2048.png new file mode 100644 index 0000000..e4c390d Binary files /dev/null and b/web/static/img/splash/apple-splash-2732-2048.png differ diff --git a/web/static/img/splash/apple-splash-2778-1284.png b/web/static/img/splash/apple-splash-2778-1284.png new file mode 100644 index 0000000..d20382b Binary files /dev/null and b/web/static/img/splash/apple-splash-2778-1284.png differ diff --git a/web/static/img/splash/apple-splash-640-1136.png b/web/static/img/splash/apple-splash-640-1136.png new file mode 100644 index 0000000..cf98ae9 Binary files /dev/null and b/web/static/img/splash/apple-splash-640-1136.png differ diff --git a/web/static/img/splash/apple-splash-750-1334.png b/web/static/img/splash/apple-splash-750-1334.png new file mode 100644 index 0000000..4524662 Binary files /dev/null and b/web/static/img/splash/apple-splash-750-1334.png differ diff --git a/web/static/img/splash/apple-splash-828-1792.png b/web/static/img/splash/apple-splash-828-1792.png new file mode 100644 index 0000000..0120f12 Binary files /dev/null and b/web/static/img/splash/apple-splash-828-1792.png differ diff --git a/web/static/img/startup.jpg b/web/static/img/startup.jpg new file mode 100644 index 0000000..4070083 Binary files /dev/null and b/web/static/img/startup.jpg differ diff --git a/web/static/img/synologychat.png b/web/static/img/synologychat.png new file mode 100644 index 0000000..40afedb Binary files /dev/null and b/web/static/img/synologychat.png differ diff --git a/web/static/img/telegram.png b/web/static/img/telegram.png new file mode 100644 index 0000000..870f25a Binary files /dev/null and b/web/static/img/telegram.png differ diff --git a/web/static/img/tmdb.png b/web/static/img/tmdb.png new file mode 100644 index 0000000..24e1668 Binary files /dev/null and b/web/static/img/tmdb.png differ diff --git a/web/static/img/tmdb.webp b/web/static/img/tmdb.webp new file mode 100644 index 0000000..10e9236 Binary files /dev/null and b/web/static/img/tmdb.webp differ diff --git a/web/static/img/transmission.png b/web/static/img/transmission.png new file mode 100644 index 0000000..13a5fbd Binary files /dev/null and b/web/static/img/transmission.png differ diff --git a/web/static/img/tv.png b/web/static/img/tv.png new file mode 100644 index 0000000..2e6ad60 Binary files /dev/null and b/web/static/img/tv.png differ diff --git a/web/static/img/users.png b/web/static/img/users.png new file mode 100644 index 0000000..5a48e1c Binary files /dev/null and b/web/static/img/users.png differ diff --git a/web/static/img/wechat.png b/web/static/img/wechat.png new file mode 100644 index 0000000..aba9f9c Binary files /dev/null and b/web/static/img/wechat.png differ diff --git a/web/static/img/work_together.svg b/web/static/img/work_together.svg new file mode 100644 index 0000000..f0b2195 --- /dev/null +++ b/web/static/img/work_together.svg @@ -0,0 +1 @@ +work_together \ No newline at end of file diff --git a/web/static/js/FileSaver.min.js b/web/static/js/FileSaver.min.js new file mode 100644 index 0000000..6d493b2 --- /dev/null +++ b/web/static/js/FileSaver.min.js @@ -0,0 +1,3 @@ +(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); + +//# sourceMappingURL=FileSaver.min.js.map \ No newline at end of file diff --git a/web/static/js/ace.js b/web/static/js/ace.js new file mode 100644 index 0000000..5760e29 --- /dev/null +++ b/web/static/js/ace.js @@ -0,0 +1,17 @@ +(function(){function o(n){var i=e;n&&(e[n]||(e[n]={}),i=e[n]);if(!i.define||!i.define.packaged)t.original=i.define,i.define=t,i.define.packaged=!0;if(!i.require||!i.require.packaged)r.original=i.require,i.require=r,i.require.packaged=!0}var ACE_NAMESPACE = "ace",e=function(){return this}();!e&&typeof window!="undefined"&&(e=window);if(!ACE_NAMESPACE&&typeof requirejs!="undefined")return;var t=function(e,n,r){if(typeof e!="string"){t.original?t.original.apply(this,arguments):(console.error("dropping module because define wasn't a string."),console.trace());return}arguments.length==2&&(r=n),t.modules[e]||(t.payloads[e]=r,t.modules[e]=null)};t.modules={},t.payloads={};var n=function(e,t,n){if(typeof t=="string"){var i=s(e,t);if(i!=undefined)return n&&n(),i}else if(Object.prototype.toString.call(t)==="[object Array]"){var o=[];for(var u=0,a=t.length;un.length)t=n.length;t-=e.length;var r=n.indexOf(e,t);return r!==-1&&r===t}),String.prototype.repeat||r(String.prototype,"repeat",function(e){var t="",n=this;while(e>0){e&1&&(t+=n);if(e>>=1)n+=n}return t}),String.prototype.includes||r(String.prototype,"includes",function(e,t){return this.indexOf(e,t)!=-1}),Object.assign||(Object.assign=function(e){if(e===undefined||e===null)throw new TypeError("Cannot convert undefined or null to object");var t=Object(e);for(var n=1;n>>0,r=arguments[1],i=r>>0,s=i<0?Math.max(n+i,0):Math.min(i,n),o=arguments[2],u=o===undefined?n:o>>0,a=u<0?Math.max(n+u,0):Math.min(u,n);while(s0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n=0?parseFloat((s.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]):parseFloat((s.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]),t.isOldIE=t.isIE&&t.isIE<9,t.isGecko=t.isMozilla=s.match(/ Gecko\/\d+/),t.isOpera=typeof opera=="object"&&Object.prototype.toString.call(window.opera)=="[object Opera]",t.isWebKit=parseFloat(s.split("WebKit/")[1])||undefined,t.isChrome=parseFloat(s.split(" Chrome/")[1])||undefined,t.isEdge=parseFloat(s.split(" Edge/")[1])||undefined,t.isAIR=s.indexOf("AdobeAIR")>=0,t.isAndroid=s.indexOf("Android")>=0,t.isChromeOS=s.indexOf(" CrOS ")>=0,t.isIOS=/iPad|iPhone|iPod/.test(s)&&!window.MSStream,t.isIOS&&(t.isMac=!0),t.isMobile=t.isIOS||t.isAndroid}),ace.define("ace/lib/dom",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";function u(){var e=o;o=null,e&&e.forEach(function(e){a(e[0],e[1])})}function a(e,n,r){if(typeof document=="undefined")return;if(o)if(r)u();else if(r===!1)return o.push([e,n]);if(s)return;var i=r;if(!r||!r.getRootNode)i=document;else{i=r.getRootNode();if(!i||i==r)i=document}var a=i.ownerDocument||i;if(n&&t.hasCssString(n,i))return null;n&&(e+="\n/*# sourceURL=ace/css/"+n+" */");var f=t.createElement("style");f.appendChild(a.createTextNode(e)),n&&(f.id=n),i==a&&(i=t.getDocumentHead(a)),i.insertBefore(f,i.firstChild)}var r=e("./useragent"),i="http://www.w3.org/1999/xhtml";t.buildDom=function l(e,t,n){if(typeof e=="string"&&e){var r=document.createTextNode(e);return t&&t.appendChild(r),r}if(!Array.isArray(e))return e&&e.appendChild&&t&&t.appendChild(e),e;if(typeof e[0]!="string"||!e[0]){var i=[];for(var s=0;s=1.5:!0,r.isChromeOS&&(t.HI_DPI=!1);if(typeof document!="undefined"){var f=document.createElement("div");t.HI_DPI&&f.style.transform!==undefined&&(t.HAS_CSS_TRANSFORMS=!0),!r.isEdge&&typeof f.style.animationName!="undefined"&&(t.HAS_CSS_ANIMATION=!0),f=null}t.HAS_CSS_TRANSFORMS?t.translate=function(e,t,n){e.style.transform="translate("+Math.round(t)+"px, "+Math.round(n)+"px)"}:t.translate=function(e,t,n){e.style.top=Math.round(n)+"px",e.style.left=Math.round(t)+"px"}}),ace.define("ace/lib/net",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./dom");t.get=function(e,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&t(n.responseText)},n.send(null)},t.loadScript=function(e,t){var n=r.getDocumentHead(),i=document.createElement("script");i.src=e,n.appendChild(i),i.onload=i.onreadystatechange=function(e,n){if(n||!i.readyState||i.readyState=="loaded"||i.readyState=="complete")i=i.onload=i.onreadystatechange=null,n||t()}},t.qualifyURL=function(e){var t=document.createElement("a");return t.href=e,t.href}}),ace.define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;o1&&(i=n[n.length-2]);var o=a[t+"Path"];return o==null?o=a.basePath:r=="/"&&(t=r=""),o&&o.slice(-1)!="/"&&(o+="/"),o+t+r+i+this.get("suffix")},t.setModuleUrl=function(e,t){return a.$moduleUrls[e]=t};var f=function(t,n){return t=="ace/theme/textmate"?n(null,e("./theme/textmate")):console.error("loader is not configured")};t.setLoader=function(e){f=e},t.$loading={},t.loadModule=function(n,r){var i,o;Array.isArray(n)&&(o=n[0],n=n[1]);try{i=e(n)}catch(u){}if(i&&!t.$loading[n])return r&&r(i);t.$loading[n]||(t.$loading[n]=[]),t.$loading[n].push(r);if(t.$loading[n].length>1)return;var a=function(){f(n,function(e,r){t._emit("load.module",{name:n,module:r});var i=t.$loading[n];t.$loading[n]=null,i.forEach(function(e){e&&e(r)})})};if(!t.get("packaged"))return a();s.loadScript(t.moduleUrl(n,o),a),l()};var l=function(){!a.basePath&&!a.workerPath&&!a.modePath&&!a.themePath&&!Object.keys(a.$moduleUrls).length&&(console.error("Unable to infer path to ace from script src,","use ace.config.set('basePath', 'path') to enable dynamic loading of modes and themes","or with webpack use ace/webpack-resolver"),l=function(){})};t.version="1.15.0"}),ace.define("ace/loader_build",["require","exports","module","ace/lib/fixoldbrowsers","ace/config"],function(e,t,n){"use strict";function s(t){if(!i||!i.document)return;r.set("packaged",t||e.packaged||n.packaged||i.define&&define.packaged);var s={},u="",a=document.currentScript||document._currentScript,f=a&&a.ownerDocument||document,l=f.getElementsByTagName("script");for(var c=0;c1?(u++,u>4&&(u=1)):u=1;if(i.isIE){var o=Math.abs(e.clientX-a)>5||Math.abs(e.clientY-f)>5;if(!l||o)u=1;l&&clearTimeout(l),l=setTimeout(function(){l=null},n[u-1]||600),u==1&&(a=e.clientX,f=e.clientY)}e._clicks=u,r[s]("mousedown",e);if(u>4)u=0;else if(u>1)return r[s](h[u],e)}var u=0,a,f,l,h={2:"dblclick",3:"tripleclick",4:"quadclick"};Array.isArray(e)||(e=[e]),e.forEach(function(e){c(e,"mousedown",p,o)})};var p=function(e){return 0|(e.ctrlKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.metaKey?8:0)};t.getModifierString=function(e){return r.KEY_MODS[p(e)]},t.addCommandKeyListener=function(e,n,r){if(i.isOldGecko||i.isOpera&&!("KeyboardEvent"in window)){var o=null;c(e,"keydown",function(e){o=e.keyCode},r),c(e,"keypress",function(e){return d(n,e,o)},r)}else{var u=null;c(e,"keydown",function(e){s[e.keyCode]=(s[e.keyCode]||0)+1;var t=d(n,e,e.keyCode);return u=e.defaultPrevented,t},r),c(e,"keypress",function(e){u&&(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey)&&(t.stopEvent(e),u=null)},r),c(e,"keyup",function(e){s[e.keyCode]=null},r),s||(v(),c(window,"focus",v))}};if(typeof window=="object"&&window.postMessage&&!i.isOldIE){var m=1;t.nextTick=function(e,n){n=n||window;var r="zero-timeout-message-"+m++,i=function(s){s.data==r&&(t.stopPropagation(s),h(n,"message",i),e())};c(n,"message",i),n.postMessage(r,"*")}}t.$idleBlocked=!1,t.onIdle=function(e,n){return setTimeout(function r(){t.$idleBlocked?setTimeout(r,100):e()},n)},t.$idleBlockId=null,t.blockIdle=function(e){t.$idleBlockId&&clearTimeout(t.$idleBlockId),t.$idleBlocked=!0,t.$idleBlockId=setTimeout(function(){t.$idleBlocked=!1},e||100)},t.nextFrame=typeof window=="object"&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame),t.nextFrame?t.nextFrame=t.nextFrame.bind(window):t.nextFrame=function(e){setTimeout(e,17)}}),ace.define("ace/range",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t){return e.row-t.row||e.column-t.column},i=function(e,t,n,r){this.start={row:e,column:t},this.end={row:n,column:r}};(function(){this.isEqual=function(e){return this.start.row===e.start.row&&this.end.row===e.end.row&&this.start.column===e.start.column&&this.end.column===e.end.column},this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?tthis.end.column?1:0:ethis.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.rowt)var r={row:t+1,column:0};else if(this.start.rowDate.now()-50?!0:r=!1},cancel:function(){r=Date.now()}}}),ace.define("ace/keyboard/textinput",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/lib/dom","ace/lib/lang","ace/clipboard","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("../lib/dom"),o=e("../lib/lang"),u=e("../clipboard"),a=i.isChrome<18,f=i.isIE,l=i.isChrome>63,c=400,h=e("../lib/keys"),p=h.KEY_MODS,d=i.isIOS,v=d?/\s/:/\n/,m=i.isMobile,g=function(e,t){function X(){x=!0,n.blur(),n.focus(),x=!1}function $(e){e.keyCode==27&&n.value.lengthC&&T[s]=="\n")o=h.end;else if(rC&&T.slice(0,s).split("\n").length>2)o=h.down;else if(s>C&&T[s-1]==" ")o=h.right,u=p.option;else if(s>C||s==C&&C!=N&&r==s)o=h.right;r!==s&&(u|=p.shift);if(o){var a=t.onCommandKey({},u,o);if(!a&&t.commands){o=h.keyCodeToString(o);var f=t.commands.findKeyCommand(u,o);f&&t.execCommand(f)}N=r,C=s,O("")}};document.addEventListener("selectionchange",s),t.on("destroy",function(){document.removeEventListener("selectionchange",s)})}var n=s.createElement("textarea");n.className="ace_text-input",n.setAttribute("wrap","off"),n.setAttribute("autocorrect","off"),n.setAttribute("autocapitalize","off"),n.setAttribute("spellcheck",!1),n.style.opacity="0",e.insertBefore(n,e.firstChild);var g=!1,y=!1,b=!1,w=!1,E="";m||(n.style.fontSize="1px");var S=!1,x=!1,T="",N=0,C=0,k=0;try{var L=document.activeElement===n}catch(A){}this.setAriaOptions=function(e){e.activeDescendant?(n.setAttribute("aria-haspopup","true"),n.setAttribute("aria-autocomplete","list"),n.setAttribute("aria-activedescendant",e.activeDescendant)):(n.setAttribute("aria-haspopup","false"),n.setAttribute("aria-autocomplete","both"),n.removeAttribute("aria-activedescendant")),e.role&&n.setAttribute("role",e.role)},this.setAriaOptions({role:"textbox"}),r.addListener(n,"blur",function(e){if(x)return;t.onBlur(e),L=!1},t),r.addListener(n,"focus",function(e){if(x)return;L=!0;if(i.isEdge)try{if(!document.hasFocus())return}catch(e){}t.onFocus(e),i.isEdge?setTimeout(O):O()},t),this.$focusScroll=!1,this.focus=function(){if(E||l||this.$focusScroll=="browser")return n.focus({preventScroll:!0});var e=n.style.top;n.style.position="fixed",n.style.top="0px";try{var t=n.getBoundingClientRect().top!=0}catch(r){return}var i=[];if(t){var s=n.parentElement;while(s&&s.nodeType==1)i.push(s),s.setAttribute("ace_nocontext",!0),!s.parentElement&&s.getRootNode?s=s.getRootNode().host:s=s.parentElement}n.focus({preventScroll:!0}),t&&i.forEach(function(e){e.removeAttribute("ace_nocontext")}),setTimeout(function(){n.style.position="",n.style.top=="0px"&&(n.style.top=e)},0)},this.blur=function(){n.blur()},this.isFocused=function(){return L},t.on("beforeEndOperation",function(){var e=t.curOp,r=e&&e.command&&e.command.name;if(r=="insertstring")return;var i=r&&(e.docChanged||e.selectionChanged);b&&i&&(T=n.value="",W()),O()});var O=d?function(e){if(!L||g&&!e||w)return;e||(e="");var r="\n ab"+e+"cde fg\n";r!=n.value&&(n.value=T=r);var i=4,s=4+(e.length||(t.selection.isEmpty()?0:1));(N!=i||C!=s)&&n.setSelectionRange(i,s),N=i,C=s}:function(){if(b||w)return;if(!L&&!P)return;b=!0;var e=0,r=0,i="";if(t.session){var s=t.selection,o=s.getRange(),u=s.cursor.row;e=o.start.column,r=o.end.column,i=t.session.getLine(u);if(o.start.row!=u){var a=t.session.getLine(u-1);e=o.start.rowu+1?f.length:r,r+=i.length+1,i=i+"\n"+f}else m&&u>0&&(i="\n"+i,r+=1,e+=1);i.length>c&&(e=T.length&&e.value===T&&T&&e.selectionEnd!==C},_=function(e){if(b)return;g?g=!1:M(n)?(t.selectAll(),O()):m&&n.selectionStart!=N&&O()},D=null;this.setInputHandler=function(e){D=e},this.getInputHandler=function(){return D};var P=!1,H=function(e,r){P&&(P=!1);if(y)return O(),e&&t.onPaste(e),y=!1,"";var s=n.selectionStart,o=n.selectionEnd,u=N,a=T.length-C,f=e,l=e.length-s,c=e.length-o,h=0;while(u>0&&T[h]==e[h])h++,u--;f=f.slice(h),h=1;while(a>0&&T.length-h>N-1&&T[T.length-h]==e[e.length-h])h++,a--;l-=h-1,c-=h-1;var p=f.length-h+1;p<0&&(u=-p,p=0),f=f.slice(0,p);if(!r&&!f&&!l&&!u&&!a&&!c)return"";w=!0;var d=!1;return i.isAndroid&&f==". "&&(f=" ",d=!0),f&&!u&&!a&&!l&&!c||S?t.onTextInput(f):t.onTextInput(f,{extendLeft:u,extendRight:a,restoreStart:l,restoreEnd:c}),w=!1,T=e,N=s,C=o,k=c,d?"\n":f},B=function(e){if(b)return z();if(e&&e.inputType){if(e.inputType=="historyUndo")return t.execCommand("undo");if(e.inputType=="historyRedo")return t.execCommand("redo")}var r=n.value,i=H(r,!0);(r.length>c+100||v.test(i)||m&&N<1&&N==C)&&O()},j=function(e,t,n){var r=e.clipboardData||window.clipboardData;if(!r||a)return;var i=f||n?"Text":"text/plain";try{return t?r.setData(i,t)!==!1:r.getData(i)}catch(e){if(!n)return j(e,t,!0)}},F=function(e,i){var s=t.getCopyText();if(!s)return r.preventDefault(e);j(e,s)?(d&&(O(s),g=s,setTimeout(function(){g=!1},10)),i?t.onCut():t.onCopy(),r.preventDefault(e)):(g=!0,n.value=s,n.select(),setTimeout(function(){g=!1,O(),i?t.onCut():t.onCopy()}))},I=function(e){F(e,!0)},q=function(e){F(e,!1)},R=function(e){var s=j(e);if(u.pasteCancelled())return;typeof s=="string"?(s&&t.onPaste(s,e),i.isIE&&setTimeout(O),r.preventDefault(e)):(n.value="",y=!0)};r.addCommandKeyListener(n,t.onCommandKey.bind(t),t),r.addListener(n,"select",_,t),r.addListener(n,"input",B,t),r.addListener(n,"cut",I,t),r.addListener(n,"copy",q,t),r.addListener(n,"paste",R,t),(!("oncut"in n)||!("oncopy"in n)||!("onpaste"in n))&&r.addListener(e,"keydown",function(e){if(i.isMac&&!e.metaKey||!e.ctrlKey)return;switch(e.keyCode){case 67:q(e);break;case 86:R(e);break;case 88:I(e)}},t);var U=function(e){if(b||!t.onCompositionStart||t.$readOnly)return;b={};if(S)return;e.data&&(b.useTextareaForIME=!1),setTimeout(z,0),t._signal("compositionStart"),t.on("mousedown",X);var r=t.getSelectionRange();r.end.row=r.start.row,r.end.column=r.start.column,b.markerRange=r,b.selectionStart=N,t.onCompositionStart(b),b.useTextareaForIME?(T=n.value="",N=0,C=0):(n.msGetInputContext&&(b.context=n.msGetInputContext()),n.getInputContext&&(b.context=n.getInputContext()))},z=function(){if(!b||!t.onCompositionUpdate||t.$readOnly)return;if(S)return X();if(b.useTextareaForIME)t.onCompositionUpdate(n.value);else{var e=n.value;H(e),b.markerRange&&(b.context&&(b.markerRange.start.column=b.selectionStart=b.context.compositionStartOffset),b.markerRange.end.column=b.markerRange.start.column+C-b.selectionStart+k)}},W=function(e){if(!t.onCompositionEnd||t.$readOnly)return;b=!1,t.onCompositionEnd(),t.off("mousedown",X),e&&B()},V=o.delayedCall(z,50).schedule.bind(null,null);r.addListener(n,"compositionstart",U,t),r.addListener(n,"compositionupdate",z,t),r.addListener(n,"keyup",$,t),r.addListener(n,"keydown",V,t),r.addListener(n,"compositionend",W,t),this.getElement=function(){return n},this.setCommandMode=function(e){S=e,n.readOnly=!1},this.setReadOnly=function(e){S||(n.readOnly=e)},this.setCopyWithEmptySelection=function(e){},this.onContextMenu=function(e){P=!0,O(),t._emit("nativecontextmenu",{target:t,domEvent:e}),this.moveToMouse(e,!0)},this.moveToMouse=function(e,o){E||(E=n.style.cssText),n.style.cssText=(o?"z-index:100000;":"")+(i.isIE?"opacity:0.1;":"")+"text-indent: -"+(N+C)*t.renderer.characterWidth*.5+"px;";var u=t.container.getBoundingClientRect(),a=s.computedStyle(t.container),f=u.top+(parseInt(a.borderTopWidth)||0),l=u.left+(parseInt(u.borderLeftWidth)||0),c=u.bottom-f-n.clientHeight-2,h=function(e){s.translate(n,e.clientX-l-2,Math.min(e.clientY-f-2,c))};h(e);if(e.type!="mousedown")return;t.renderer.$isMousePressed=!0,clearTimeout(J),i.isWin&&r.capture(t.container,h,K)},this.onContextMenuClose=K;var J,Q=function(e){t.textInput.onContextMenu(e),K()};r.addListener(n,"mouseup",Q,t),r.addListener(n,"mousedown",function(e){e.preventDefault(),K()},t),r.addListener(t.renderer.scroller,"contextmenu",Q,t),r.addListener(n,"contextmenu",Q,t),d&&G(e,t,n),this.destroy=function(){n.parentElement&&n.parentElement.removeChild(n)}};t.TextInput=g,t.$setUserAgentForTests=function(e,t){m=e,d=t}}),ace.define("ace/mouse/default_handlers",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";function o(e){e.$clickSelection=null;var t=e.editor;t.setDefaultHandler("mousedown",this.onMouseDown.bind(e)),t.setDefaultHandler("dblclick",this.onDoubleClick.bind(e)),t.setDefaultHandler("tripleclick",this.onTripleClick.bind(e)),t.setDefaultHandler("quadclick",this.onQuadClick.bind(e)),t.setDefaultHandler("mousewheel",this.onMouseWheel.bind(e));var n=["select","startSelect","selectEnd","selectAllEnd","selectByWordsEnd","selectByLinesEnd","dragWait","dragWaitEnd","focusWait"];n.forEach(function(t){e[t]=this[t]},this),e.selectByLines=this.extendSelectionBy.bind(e,"getLineRange"),e.selectByWords=this.extendSelectionBy.bind(e,"getWordRange")}function u(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}function a(e,t){if(e.start.row==e.end.row)var n=2*t.column-e.start.column-e.end.column;else if(e.start.row==e.end.row-1&&!e.start.column&&!e.end.column)var n=t.column-4;else var n=2*t.row-e.start.row-e.end.row;return n<0?{cursor:e.start,anchor:e.end}:{cursor:e.end,anchor:e.start}}var r=e("../lib/useragent"),i=0,s=550;(function(){this.onMouseDown=function(e){var t=e.inSelection(),n=e.getDocumentPosition();this.mousedownEvent=e;var i=this.editor,s=e.getButton();if(s!==0){var o=i.getSelectionRange(),u=o.isEmpty();(u||s==1)&&i.selection.moveToPosition(n),s==2&&(i.textInput.onContextMenu(e.domEvent),r.isMozilla||e.preventDefault());return}this.mousedownEvent.time=Date.now();if(t&&!i.isFocused()){i.focus();if(this.$focusTimeout&&!this.$clickSelection&&!i.inMultiSelectMode){this.setState("focusWait"),this.captureMouse(e);return}}return this.captureMouse(e),this.startSelect(n,e.domEvent._clicks>1),e.preventDefault()},this.startSelect=function(e,t){e=e||this.editor.renderer.screenToTextCoordinates(this.x,this.y);var n=this.editor;if(!this.mousedownEvent)return;this.mousedownEvent.getShiftKey()?n.selection.selectToPosition(e):t||n.selection.moveToPosition(e),t||this.select(),n.renderer.scroller.setCapture&&n.renderer.scroller.setCapture(),n.setStyle("ace_selecting"),this.setState("select")},this.select=function(){var e,t=this.editor,n=t.renderer.screenToTextCoordinates(this.x,this.y);if(this.$clickSelection){var r=this.$clickSelection.comparePoint(n);if(r==-1)e=this.$clickSelection.end;else if(r==1)e=this.$clickSelection.start;else{var i=a(this.$clickSelection,n);n=i.cursor,e=i.anchor}t.selection.setSelectionAnchor(e.row,e.column)}t.selection.selectToPosition(n),t.renderer.scrollCursorIntoView()},this.extendSelectionBy=function(e){var t,n=this.editor,r=n.renderer.screenToTextCoordinates(this.x,this.y),i=n.selection[e](r.row,r.column);if(this.$clickSelection){var s=this.$clickSelection.comparePoint(i.start),o=this.$clickSelection.comparePoint(i.end);if(s==-1&&o<=0){t=this.$clickSelection.end;if(i.end.row!=r.row||i.end.column!=r.column)r=i.start}else if(o==1&&s>=0){t=this.$clickSelection.start;if(i.start.row!=r.row||i.start.column!=r.column)r=i.end}else if(s==-1&&o==1)r=i.end,t=i.start;else{var u=a(this.$clickSelection,r);r=u.cursor,t=u.anchor}n.selection.setSelectionAnchor(t.row,t.column)}n.selection.selectToPosition(r),n.renderer.scrollCursorIntoView()},this.selectEnd=this.selectAllEnd=this.selectByWordsEnd=this.selectByLinesEnd=function(){this.$clickSelection=null,this.editor.unsetStyle("ace_selecting"),this.editor.renderer.scroller.releaseCapture&&this.editor.renderer.scroller.releaseCapture()},this.focusWait=function(){var e=u(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y),t=Date.now();(e>i||t-this.mousedownEvent.time>this.$focusTimeout)&&this.startSelect(this.mousedownEvent.getDocumentPosition())},this.onDoubleClick=function(e){var t=e.getDocumentPosition(),n=this.editor,r=n.session,i=r.getBracketRange(t);i?(i.isEmpty()&&(i.start.column--,i.end.column++),this.setState("select")):(i=n.selection.getWordRange(t.row,t.column),this.setState("selectByWords")),this.$clickSelection=i,this.select()},this.onTripleClick=function(e){var t=e.getDocumentPosition(),n=this.editor;this.setState("selectByLines");var r=n.getSelectionRange();r.isMultiLine()&&r.contains(t.row,t.column)?(this.$clickSelection=n.selection.getLineRange(r.start.row),this.$clickSelection.end=n.selection.getLineRange(r.end.row).end):this.$clickSelection=n.selection.getLineRange(t.row),this.select()},this.onQuadClick=function(e){var t=this.editor;t.selectAll(),this.$clickSelection=t.getSelectionRange(),this.setState("selectAll")},this.onMouseWheel=function(e){if(e.getAccelKey())return;e.getShiftKey()&&e.wheelY&&!e.wheelX&&(e.wheelX=e.wheelY,e.wheelY=0);var t=this.editor;this.$lastScroll||(this.$lastScroll={t:0,vx:0,vy:0,allowed:0});var n=this.$lastScroll,r=e.domEvent.timeStamp,i=r-n.t,o=i?e.wheelX/i:n.vx,u=i?e.wheelY/i:n.vy;i=1&&t.renderer.isScrollableBy(e.wheelX*e.speed,0)&&(f=!0),a<=1&&t.renderer.isScrollableBy(0,e.wheelY*e.speed)&&(f=!0);if(f)n.allowed=r;else if(r-n.allowedt.session.documentToScreenRow(l.row,l.column))return c()}if(f==s)return;f=s.text.join("
"),i.setHtml(f);var p=s.className;p&&i.setClassName(p.trim()),i.show(),t._signal("showGutterTooltip",i),t.on("mousewheel",c);if(e.$tooltipFollowsMouse)h(u);else{var d=u.domEvent.target,v=d.getBoundingClientRect(),m=i.getElement().style;m.left=v.right+"px",m.top=v.bottom+"px"}}function c(){o&&(o=clearTimeout(o)),f&&(i.hide(),f=null,t._signal("hideGutterTooltip",i),t.off("mousewheel",c))}function h(e){i.setPosition(e.x,e.y)}var t=e.editor,n=t.renderer.$gutterLayer,i=new a(t.container);e.editor.setDefaultHandler("guttermousedown",function(r){if(!t.isFocused()||r.getButton()!=0)return;var i=n.getRegion(r);if(i=="foldWidgets")return;var s=r.getDocumentPosition().row,o=t.session.selection;if(r.getShiftKey())o.selectTo(s,0);else{if(r.domEvent.detail==2)return t.selectAll(),r.preventDefault();e.$clickSelection=t.selection.getLineRange(s)}return e.setState("selectByLines"),e.captureMouse(r),r.preventDefault()});var o,u,f;e.editor.setDefaultHandler("guttermousemove",function(t){var n=t.domEvent.target||t.domEvent.srcElement;if(r.hasCssClass(n,"ace_fold-widget"))return c();f&&e.$tooltipFollowsMouse&&h(t),u=t;if(o)return;o=setTimeout(function(){o=null,u&&!e.isMousePressed?l():c()},50)}),s.addListener(t.renderer.$gutter,"mouseout",function(e){u=null;if(!f||o)return;o=setTimeout(function(){o=null,c()},50)},t),t.on("changeSession",c)}function a(e){o.call(this,e)}var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event"),o=e("../tooltip").Tooltip;i.inherits(a,o),function(){this.setPosition=function(e,t){var n=window.innerWidth||document.documentElement.clientWidth,r=window.innerHeight||document.documentElement.clientHeight,i=this.getWidth(),s=this.getHeight();e+=15,t+=15,e+i>n&&(e-=e+i-n),t+s>r&&(t-=20+s),o.prototype.setPosition.call(this,e,t)}}.call(a.prototype),t.GutterHandler=u}),ace.define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=t.MouseEvent=function(e,t){this.domEvent=e,this.editor=t,this.x=this.clientX=e.clientX,this.y=this.clientY=e.clientY,this.$pos=null,this.$inSelection=null,this.propagationStopped=!1,this.defaultPrevented=!1};(function(){this.stopPropagation=function(){r.stopPropagation(this.domEvent),this.propagationStopped=!0},this.preventDefault=function(){r.preventDefault(this.domEvent),this.defaultPrevented=!0},this.stop=function(){this.stopPropagation(),this.preventDefault()},this.getDocumentPosition=function(){return this.$pos?this.$pos:(this.$pos=this.editor.renderer.screenToTextCoordinates(this.clientX,this.clientY),this.$pos)},this.inSelection=function(){if(this.$inSelection!==null)return this.$inSelection;var e=this.editor,t=e.getSelectionRange();if(t.isEmpty())this.$inSelection=!1;else{var n=this.getDocumentPosition();this.$inSelection=t.contains(n.row,n.column)}return this.$inSelection},this.getButton=function(){return r.getButton(this.domEvent)},this.getShiftKey=function(){return this.domEvent.shiftKey},this.getAccelKey=i.isMac?function(){return this.domEvent.metaKey}:function(){return this.domEvent.ctrlKey}}).call(s.prototype)}),ace.define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function f(e){function T(e,n){var r=Date.now(),i=!n||e.row!=n.row,s=!n||e.column!=n.column;if(!S||i||s)t.moveCursorToPosition(e),S=r,x={x:p,y:d};else{var o=l(x.x,x.y,p,d);o>a?S=null:r-S>=u&&(t.renderer.scrollCursorIntoView(),S=null)}}function N(e,n){var r=Date.now(),i=t.renderer.layerConfig.lineHeight,s=t.renderer.layerConfig.characterWidth,u=t.renderer.scroller.getBoundingClientRect(),a={x:{left:p-u.left,right:u.right-p},y:{top:d-u.top,bottom:u.bottom-d}},f=Math.min(a.x.left,a.x.right),l=Math.min(a.y.top,a.y.bottom),c={row:e.row,column:e.column};f/s<=2&&(c.column+=a.x.left=o&&t.renderer.scrollCursorIntoView(c):E=r:E=null}function C(){var e=g;g=t.renderer.screenToTextCoordinates(p,d),T(g,e),N(g,e)}function k(){m=t.selection.toOrientedRange(),h=t.session.addMarker(m,"ace_selection",t.getSelectionStyle()),t.clearSelection(),t.isFocused()&&t.renderer.$cursorLayer.setBlinking(!1),clearInterval(v),C(),v=setInterval(C,20),y=0,i.addListener(document,"mousemove",O)}function L(){clearInterval(v),t.session.removeMarker(h),h=null,t.selection.fromOrientedRange(m),t.isFocused()&&!w&&t.$resetCursorStyle(),m=null,g=null,y=0,E=null,S=null,i.removeListener(document,"mousemove",O)}function O(){A==null&&(A=setTimeout(function(){A!=null&&h&&L()},20))}function M(e){var t=e.types;return!t||Array.prototype.some.call(t,function(e){return e=="text/plain"||e=="Text"})}function _(e){var t=["copy","copymove","all","uninitialized"],n=["move","copymove","linkmove","all","uninitialized"],r=s.isMac?e.altKey:e.ctrlKey,i="uninitialized";try{i=e.dataTransfer.effectAllowed.toLowerCase()}catch(e){}var o="none";return r&&t.indexOf(i)>=0?o="copy":n.indexOf(i)>=0?o="move":t.indexOf(i)>=0&&(o="copy"),o}var t=e.editor,n=r.createElement("div");n.style.cssText="top:-100px;position:absolute;z-index:2147483647;opacity:0.5",n.textContent="\u00a0";var f=["dragWait","dragWaitEnd","startDrag","dragReadyEnd","onMouseDrag"];f.forEach(function(t){e[t]=this[t]},this),t.on("mousedown",this.onMouseDown.bind(e));var c=t.container,h,p,d,v,m,g,y=0,b,w,E,S,x;this.onDragStart=function(e){if(this.cancelDrag||!c.draggable){var r=this;return setTimeout(function(){r.startSelect(),r.captureMouse(e)},0),e.preventDefault()}m=t.getSelectionRange();var i=e.dataTransfer;i.effectAllowed=t.getReadOnly()?"copy":"copyMove",t.container.appendChild(n),i.setDragImage&&i.setDragImage(n,0,0),setTimeout(function(){t.container.removeChild(n)}),i.clearData(),i.setData("Text",t.session.getTextRange()),w=!0,this.setState("drag")},this.onDragEnd=function(e){c.draggable=!1,w=!1,this.setState(null);if(!t.getReadOnly()){var n=e.dataTransfer.dropEffect;!b&&n=="move"&&t.session.remove(t.getSelectionRange()),t.$resetCursorStyle()}this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle("")},this.onDragEnter=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||k(),y++,e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragOver=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||(k(),y++),A!==null&&(A=null),e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragLeave=function(e){y--;if(y<=0&&h)return L(),b=null,i.preventDefault(e)},this.onDrop=function(e){if(!g)return;var n=e.dataTransfer;if(w)switch(b){case"move":m.contains(g.row,g.column)?m={start:g,end:g}:m=t.moveText(m,g);break;case"copy":m=t.moveText(m,g,!0)}else{var r=n.getData("Text");m={start:g,end:t.session.insert(g,r)},t.focus(),b=null}return L(),i.preventDefault(e)},i.addListener(c,"dragstart",this.onDragStart.bind(e),t),i.addListener(c,"dragend",this.onDragEnd.bind(e),t),i.addListener(c,"dragenter",this.onDragEnter.bind(e),t),i.addListener(c,"dragover",this.onDragOver.bind(e),t),i.addListener(c,"dragleave",this.onDragLeave.bind(e),t),i.addListener(c,"drop",this.onDrop.bind(e),t);var A=null}function l(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=200,u=200,a=5;(function(){this.dragWait=function(){var e=Date.now()-this.mousedownEvent.time;e>this.editor.getDragDelay()&&this.startDrag()},this.dragWaitEnd=function(){var e=this.editor.container;e.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()),this.selectEnd()},this.dragReadyEnd=function(e){this.editor.$resetCursorStyle(),this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle(""),this.dragWaitEnd()},this.startDrag=function(){this.cancelDrag=!1;var e=this.editor,t=e.container;t.draggable=!0,e.renderer.$cursorLayer.setBlinking(!1),e.setStyle("ace_dragging");var n=s.isWin?"default":"move";e.renderer.setCursorStyle(n),this.setState("dragReady")},this.onMouseDrag=function(e){var t=this.editor.container;if(s.isIE&&this.state=="dragReady"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>3&&t.dragDrop()}if(this.state==="dragWait"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>0&&(t.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()))}},this.onMouseDown=function(e){if(!this.$dragEnabled)return;this.mousedownEvent=e;var t=this.editor,n=e.inSelection(),r=e.getButton(),i=e.domEvent.detail||1;if(i===1&&r===0&&n){if(e.editor.inMultiSelectMode&&(e.getAccelKey()||e.getShiftKey()))return;this.mousedownEvent.time=Date.now();var o=e.domEvent.target||e.domEvent.srcElement;"unselectable"in o&&(o.unselectable="on");if(t.getDragDelay()){if(s.isWebKit){this.cancelDrag=!0;var u=t.container;u.draggable=!0}this.setState("dragWait")}else this.startDrag();this.captureMouse(e,this.onMouseDrag.bind(this)),e.defaultPrevented=!0}}}).call(f.prototype),t.DragdropHandler=f}),ace.define("ace/mouse/touch_handler",["require","exports","module","ace/mouse/mouse_event","ace/lib/event","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./mouse_event").MouseEvent,i=e("../lib/event"),s=e("../lib/dom");t.addTouchListeners=function(e,t){function b(){var e=window.navigator&&window.navigator.clipboard,r=!1,i=function(){var n=t.getCopyText(),i=t.session.getUndoManager().hasUndo();y.replaceChild(s.buildDom(r?["span",!n&&["span",{"class":"ace_mobile-button",action:"selectall"},"Select All"],n&&["span",{"class":"ace_mobile-button",action:"copy"},"Copy"],n&&["span",{"class":"ace_mobile-button",action:"cut"},"Cut"],e&&["span",{"class":"ace_mobile-button",action:"paste"},"Paste"],i&&["span",{"class":"ace_mobile-button",action:"undo"},"Undo"],["span",{"class":"ace_mobile-button",action:"find"},"Find"],["span",{"class":"ace_mobile-button",action:"openCommandPallete"},"Palette"]]:["span"]),y.firstChild)},o=function(n){var s=n.target.getAttribute("action");if(s=="more"||!r)return r=!r,i();if(s=="paste")e.readText().then(function(e){t.execCommand(s,e)});else if(s){if(s=="cut"||s=="copy")e?e.writeText(t.getCopyText()):document.execCommand("copy");t.execCommand(s)}y.firstChild.style.display="none",r=!1,s!="openCommandPallete"&&t.focus()};y=s.buildDom(["div",{"class":"ace_mobile-menu",ontouchstart:function(e){n="menu",e.stopPropagation(),e.preventDefault(),t.textInput.focus()},ontouchend:function(e){e.stopPropagation(),e.preventDefault(),o(e)},onclick:o},["span"],["span",{"class":"ace_mobile-button",action:"more"},"..."]],t.container)}function w(){y||b();var e=t.selection.cursor,n=t.renderer.textToScreenCoordinates(e.row,e.column),r=t.renderer.textToScreenCoordinates(0,0).pageX,i=t.renderer.scrollLeft,s=t.container.getBoundingClientRect();y.style.top=n.pageY-s.top-3+"px",n.pageX-s.left=2?t.selection.getLineRange(p.row):t.session.getBracketRange(p);e&&!e.isEmpty()?t.selection.setRange(e):t.selection.selectWord(),n="wait"}function T(){h+=60,c=setInterval(function(){h--<=0&&(clearInterval(c),c=null),Math.abs(v)<.01&&(v=0),Math.abs(m)<.01&&(m=0),h<20&&(v=.9*v),h<20&&(m=.9*m);var e=t.session.getScrollTop();t.renderer.scrollBy(10*v,10*m),e==t.session.getScrollTop()&&(h=0)},10)}var n="scroll",o,u,a,f,l,c,h=0,p,d=0,v=0,m=0,g,y;i.addListener(e,"contextmenu",function(e){if(!g)return;var n=t.textInput.getElement();n.focus()},t),i.addListener(e,"touchstart",function(e){var i=e.touches;if(l||i.length>1){clearTimeout(l),l=null,a=-1,n="zoom";return}g=t.$mouseHandler.isMousePressed=!0;var s=t.renderer.layerConfig.lineHeight,c=t.renderer.layerConfig.lineHeight,y=e.timeStamp;f=y;var b=i[0],w=b.clientX,E=b.clientY;Math.abs(o-w)+Math.abs(u-E)>s&&(a=-1),o=e.clientX=w,u=e.clientY=E,v=m=0;var T=new r(e,t);p=T.getDocumentPosition();if(y-a<500&&i.length==1&&!h)d++,e.preventDefault(),e.button=0,x();else{d=0;var N=t.selection.cursor,C=t.selection.isEmpty()?N:t.selection.anchor,k=t.renderer.$cursorLayer.getPixelPosition(N,!0),L=t.renderer.$cursorLayer.getPixelPosition(C,!0),A=t.renderer.scroller.getBoundingClientRect(),O=t.renderer.layerConfig.offset,M=t.renderer.scrollLeft,_=function(e,t){return e/=c,t=t/s-.75,e*e+t*t};if(e.clientXP?"cursor":"anchor"),P<3.5?n="anchor":D<3.5?n="cursor":n="scroll",l=setTimeout(S,450)}a=y},t),i.addListener(e,"touchend",function(e){g=t.$mouseHandler.isMousePressed=!1,c&&clearInterval(c),n=="zoom"?(n="",h=0):l?(t.selection.moveToPosition(p),h=0,w()):n=="scroll"?(T(),E()):w(),clearTimeout(l),l=null},t),i.addListener(e,"touchmove",function(e){l&&(clearTimeout(l),l=null);var i=e.touches;if(i.length>1||n=="zoom")return;var s=i[0],a=o-s.clientX,c=u-s.clientY;if(n=="wait"){if(!(a*a+c*c>4))return e.preventDefault();n="cursor"}o=s.clientX,u=s.clientY,e.clientX=s.clientX,e.clientY=s.clientY;var h=e.timeStamp,p=h-f;f=h;if(n=="scroll"){var d=new r(e,t);d.speed=1,d.wheelX=a,d.wheelY=c,10*Math.abs(a)0)if(g==16){for(w=b;w-1){for(w=b;w=0;C--){if(r[C]!=N)break;t[C]=s}}}function I(e,t,n){if(o=e){u=i+1;while(u=e)u++;for(a=i,l=u-1;a=t.length||(o=n[r-1])!=b&&o!=w||(c=t[r+1])!=b&&c!=w)return E;return u&&(c=w),c==o?c:E;case k:o=r>0?n[r-1]:S;if(o==b&&r+10&&n[r-1]==b)return b;if(u)return E;p=r+1,h=t.length;while(p=1425&&d<=2303||d==64286;o=t[p];if(v&&(o==y||o==T))return y}if(r<1||(o=t[r-1])==S)return E;return n[r-1];case S:return u=!1,f=!0,s;case x:return l=!0,E;case O:case M:case D:case P:case _:u=!1;case H:return E}}function R(e){var t=e.charCodeAt(0),n=t>>8;return n==0?t>191?g:B[t]:n==5?/[\u0591-\u05f4]/.test(e)?y:g:n==6?/[\u0610-\u061a\u064b-\u065f\u06d6-\u06e4\u06e7-\u06ed]/.test(e)?A:/[\u0660-\u0669\u066b-\u066c]/.test(e)?w:t==1642?L:/[\u06f0-\u06f9]/.test(e)?b:T:n==32&&t<=8287?j[t&255]:n==254?t>=65136?T:E:E}function U(e){return e>="\u064b"&&e<="\u0655"}var r=["\u0621","\u0641"],i=["\u063a","\u064a"],s=0,o=0,u=!1,a=!1,f=!1,l=!1,c=!1,h=!1,p=[[0,3,0,1,0,0,0],[0,3,0,1,2,2,0],[0,3,0,17,2,0,1],[0,3,5,5,4,1,0],[0,3,21,21,4,0,1],[0,3,5,5,4,2,0]],d=[[2,0,1,1,0,1,0],[2,0,1,1,0,2,0],[2,0,2,1,3,2,0],[2,0,2,33,3,1,1]],v=0,m=1,g=0,y=1,b=2,w=3,E=4,S=5,x=6,T=7,N=8,C=9,k=10,L=11,A=12,O=13,M=14,_=15,D=16,P=17,H=18,B=[H,H,H,H,H,H,H,H,H,x,S,x,N,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,S,S,S,x,N,E,E,L,L,L,E,E,E,E,E,k,C,k,C,C,b,b,b,b,b,b,b,b,b,b,C,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,H,H,H,H,H,H,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,C,E,L,L,L,L,E,E,E,E,g,E,E,H,E,E,L,L,b,b,E,g,E,E,E,b,g,E,E,E,E,E],j=[N,N,N,N,N,N,N,N,N,N,N,H,H,H,g,y,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N,S,O,M,_,D,P,C,L,L,L,L,L,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,C,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N];t.L=g,t.R=y,t.EN=b,t.ON_R=3,t.AN=4,t.R_H=5,t.B=6,t.RLE=7,t.DOT="\u00b7",t.doBidiReorder=function(e,n,r){if(e.length<2)return{};var i=e.split(""),o=new Array(i.length),u=new Array(i.length),a=[];s=r?m:v,F(i,a,i.length,n);for(var f=0;fT&&n[f]0&&i[f-1]==="\u0644"&&/\u0622|\u0623|\u0625|\u0627/.test(i[f])&&(a[f-1]=a[f]=t.R_H,f++);i[i.length-1]===t.DOT&&(a[i.length-1]=t.B),i[0]==="\u202b"&&(a[0]=t.RLE);for(var f=0;f=0&&(e=this.session.$docRowCache[n])}return e},this.getSplitIndex=function(){var e=0,t=this.session.$screenRowCache;if(t.length){var n,r=this.session.$getRowCacheIndex(t,this.currentRow);while(this.currentRow-e>0){n=this.session.$getRowCacheIndex(t,this.currentRow-e-1);if(n!==r)break;r=n,e++}}else e=this.currentRow;return e},this.updateRowLine=function(e,t){e===undefined&&(e=this.getDocumentRow());var n=e===this.session.getLength()-1,s=n?this.EOF:this.EOL;this.wrapIndent=0,this.line=this.session.getLine(e),this.isRtlDir=this.$isRtl||this.line.charAt(0)===this.RLE;if(this.session.$useWrapMode){var o=this.session.$wrapData[e];o&&(t===undefined&&(t=this.getSplitIndex()),t>0&&o.length?(this.wrapIndent=o.indent,this.wrapOffset=this.wrapIndent*this.charWidths[r.L],this.line=tt?this.session.getOverwrite()?e:e-1:t,i=r.getVisualFromLogicalIdx(n,this.bidiMap),s=this.bidiMap.bidiLevels,o=0;!this.session.getOverwrite()&&e<=t&&s[i]%2!==0&&i++;for(var u=0;ut&&s[i]%2===0&&(o+=this.charWidths[s[i]]),this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset),this.isRtlDir&&(o+=this.rtlLineOffset),o},this.getSelections=function(e,t){var n=this.bidiMap,r=n.bidiLevels,i,s=[],o=0,u=Math.min(e,t)-this.wrapIndent,a=Math.max(e,t)-this.wrapIndent,f=!1,l=!1,c=0;this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset);for(var h,p=0;p=u&&hn+s/2){n+=s;if(r===i.length-1){s=0;break}s=this.charWidths[i[++r]]}return r>0&&i[r-1]%2!==0&&i[r]%2===0?(e0&&i[r-1]%2===0&&i[r]%2!==0?t=1+(e>n?this.bidiMap.logicalFromVisual[r]:this.bidiMap.logicalFromVisual[r-1]):this.isRtlDir&&r===i.length-1&&s===0&&i[r-1]%2===0||!this.isRtlDir&&r===0&&i[r]%2!==0?t=1+this.bidiMap.logicalFromVisual[r]:(r>0&&i[r-1]%2!==0&&s!==0&&r--,t=this.bidiMap.logicalFromVisual[r]),t===0&&this.isRtlDir&&t++,t+this.wrapIndent}}).call(o.prototype),t.BidiHandler=o}),ace.define("ace/selection",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/range"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=function(e){this.session=e,this.doc=e.getDocument(),this.clearSelection(),this.cursor=this.lead=this.doc.createAnchor(0,0),this.anchor=this.doc.createAnchor(0,0),this.$silent=!1;var t=this;this.cursor.on("change",function(e){t.$cursorChanged=!0,t.$silent||t._emit("changeCursor"),!t.$isEmpty&&!t.$silent&&t._emit("changeSelection"),!t.$keepDesiredColumnOnChange&&e.old.column!=e.value.column&&(t.$desiredColumn=null)}),this.anchor.on("change",function(){t.$anchorChanged=!0,!t.$isEmpty&&!t.$silent&&t._emit("changeSelection")})};(function(){r.implement(this,s),this.isEmpty=function(){return this.$isEmpty||this.anchor.row==this.lead.row&&this.anchor.column==this.lead.column},this.isMultiLine=function(){return!this.$isEmpty&&this.anchor.row!=this.cursor.row},this.getCursor=function(){return this.lead.getPosition()},this.setSelectionAnchor=function(e,t){this.$isEmpty=!1,this.anchor.setPosition(e,t)},this.getAnchor=this.getSelectionAnchor=function(){return this.$isEmpty?this.getSelectionLead():this.anchor.getPosition()},this.getSelectionLead=function(){return this.lead.getPosition()},this.isBackwards=function(){var e=this.anchor,t=this.lead;return e.row>t.row||e.row==t.row&&e.column>t.column},this.getRange=function(){var e=this.anchor,t=this.lead;return this.$isEmpty?o.fromPoints(t,t):this.isBackwards()?o.fromPoints(t,e):o.fromPoints(e,t)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._emit("changeSelection"))},this.selectAll=function(){this.$setSelection(0,0,Number.MAX_VALUE,Number.MAX_VALUE)},this.setRange=this.setSelectionRange=function(e,t){var n=t?e.end:e.start,r=t?e.start:e.end;this.$setSelection(n.row,n.column,r.row,r.column)},this.$setSelection=function(e,t,n,r){if(this.$silent)return;var i=this.$isEmpty,s=this.inMultiSelectMode;this.$silent=!0,this.$cursorChanged=this.$anchorChanged=!1,this.anchor.setPosition(e,t),this.cursor.setPosition(n,r),this.$isEmpty=!o.comparePoints(this.anchor,this.cursor),this.$silent=!1,this.$cursorChanged&&this._emit("changeCursor"),(this.$cursorChanged||this.$anchorChanged||i!=this.$isEmpty||s)&&this._emit("changeSelection")},this.$moveSelection=function(e){var t=this.lead;this.$isEmpty&&this.setSelectionAnchor(t.row,t.column),e.call(this)},this.selectTo=function(e,t){this.$moveSelection(function(){this.moveCursorTo(e,t)})},this.selectToPosition=function(e){this.$moveSelection(function(){this.moveCursorToPosition(e)})},this.moveTo=function(e,t){this.clearSelection(),this.moveCursorTo(e,t)},this.moveToPosition=function(e){this.clearSelection(),this.moveCursorToPosition(e)},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.getWordRange=function(e,t){if(typeof t=="undefined"){var n=e||this.lead;e=n.row,t=n.column}return this.session.getWordRange(e,t)},this.selectWord=function(){this.setSelectionRange(this.getWordRange())},this.selectAWord=function(){var e=this.getCursor(),t=this.session.getAWordRange(e.row,e.column);this.setSelectionRange(t)},this.getLineRange=function(e,t){var n=typeof e=="number"?e:this.lead.row,r,i=this.session.getFoldLine(n);return i?(n=i.start.row,r=i.end.row):r=n,t===!0?new o(n,0,r,this.session.getLine(r).length):new o(n,0,r+1,0)},this.selectLine=function(){this.setSelectionRange(this.getLineRange())},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.wouldMoveIntoSoftTab=function(e,t,n){var r=e.column,i=e.column+t;return n<0&&(r=e.column-t,i=e.column),this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(r,i).split(" ").length-1==t},this.moveCursorLeft=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,-1))this.moveCursorTo(t.start.row,t.start.column);else if(e.column===0)e.row>0&&this.moveCursorTo(e.row-1,this.doc.getLine(e.row-1).length);else{var n=this.session.getTabSize();this.wouldMoveIntoSoftTab(e,n,-1)&&!this.session.getNavigateWithinSoftTabs()?this.moveCursorBy(0,-n):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,1))this.moveCursorTo(t.end.row,t.end.column);else if(this.lead.column==this.doc.getLine(this.lead.row).length)this.lead.row0&&(t.column=r)}}this.moveCursorTo(t.row,t.column)},this.moveCursorFileEnd=function(){var e=this.doc.getLength()-1,t=this.doc.getLine(e).length;this.moveCursorTo(e,t)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorLongWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t);this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var i=this.session.getFoldAt(e,t,1);if(i){this.moveCursorTo(i.end.row,i.end.column);return}this.session.nonTokenRe.exec(r)&&(t+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0,r=n.substring(t));if(t>=n.length){this.moveCursorTo(e,n.length),this.moveCursorRight(),e0&&this.moveCursorWordLeft();return}this.session.tokenRe.exec(s)&&(t-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0),this.moveCursorTo(e,t)},this.$shortWordEndIndex=function(e){var t=0,n,r=/\s/,i=this.session.tokenRe;i.lastIndex=0;if(this.session.tokenRe.exec(e))t=this.session.tokenRe.lastIndex;else{while((n=e[t])&&r.test(n))t++;if(t<1){i.lastIndex=0;while((n=e[t])&&!i.test(n)){i.lastIndex=0,t++;if(r.test(n)){if(t>2){t--;break}while((n=e[t])&&r.test(n))t++;if(t>2)break}}}}return i.lastIndex=0,t},this.moveCursorShortWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i=this.session.getFoldAt(e,t,1);if(i)return this.moveCursorTo(i.end.row,i.end.column);if(t==n.length){var s=this.doc.getLength();do e++,r=this.doc.getLine(e);while(e0&&/^\s*$/.test(r));t=r.length,/\s+$/.test(r)||(r="")}var s=i.stringReverse(r),o=this.$shortWordEndIndex(s);return this.moveCursorTo(e,t-o)},this.moveCursorWordRight=function(){this.session.$selectLongWords?this.moveCursorLongWordRight():this.moveCursorShortWordRight()},this.moveCursorWordLeft=function(){this.session.$selectLongWords?this.moveCursorLongWordLeft():this.moveCursorShortWordLeft()},this.moveCursorBy=function(e,t){var n=this.session.documentToScreenPosition(this.lead.row,this.lead.column),r;t===0&&(e!==0&&(this.session.$bidiHandler.isBidiRow(n.row,this.lead.row)?(r=this.session.$bidiHandler.getPosLeft(n.column),n.column=Math.round(r/this.session.$bidiHandler.charWidths[0])):r=n.column*this.session.$bidiHandler.charWidths[0]),this.$desiredColumn?n.column=this.$desiredColumn:this.$desiredColumn=n.column);if(e!=0&&this.session.lineWidgets&&this.session.lineWidgets[this.lead.row]){var i=this.session.lineWidgets[this.lead.row];e<0?e-=i.rowsAbove||0:e>0&&(e+=i.rowCount-(i.rowsAbove||0))}var s=this.session.screenToDocumentPosition(n.row+e,n.column,r);e!==0&&t===0&&s.row===this.lead.row&&s.column===this.lead.column,this.moveCursorTo(s.row,s.column+t,t===0)},this.moveCursorToPosition=function(e){this.moveCursorTo(e.row,e.column)},this.moveCursorTo=function(e,t,n){var r=this.session.getFoldAt(e,t,1);r&&(e=r.start.row,t=r.start.column),this.$keepDesiredColumnOnChange=!0;var i=this.session.getLine(e);/[\uDC00-\uDFFF]/.test(i.charAt(t))&&i.charAt(t-1)&&(this.lead.row==e&&this.lead.column==t+1?t-=1:t+=1),this.lead.setPosition(e,t),this.$keepDesiredColumnOnChange=!1,n||(this.$desiredColumn=null)},this.moveCursorToScreen=function(e,t,n){var r=this.session.screenToDocumentPosition(e,t);this.moveCursorTo(r.row,r.column,n)},this.detach=function(){this.lead.detach(),this.anchor.detach()},this.fromOrientedRange=function(e){this.setSelectionRange(e,e.cursor==e.start),this.$desiredColumn=e.desiredColumn||this.$desiredColumn},this.toOrientedRange=function(e){var t=this.getRange();return e?(e.start.column=t.start.column,e.start.row=t.start.row,e.end.column=t.end.column,e.end.row=t.end.row):e=t,e.cursor=this.isBackwards()?e.start:e.end,e.desiredColumn=this.$desiredColumn,e},this.getRangeOfMovements=function(e){var t=this.getCursor();try{e(this);var n=this.getCursor();return o.fromPoints(t,n)}catch(r){return o.fromPoints(t,t)}finally{this.moveCursorToPosition(t)}},this.toJSON=function(){if(this.rangeCount)var e=this.ranges.map(function(e){var t=e.clone();return t.isBackwards=e.cursor==e.start,t});else{var e=this.getRange();e.isBackwards=this.isBackwards()}return e},this.fromJSON=function(e){if(e.start==undefined){if(this.rangeList&&e.length>1){this.toSingleRange(e[0]);for(var t=e.length;t--;){var n=o.fromPoints(e[t].start,e[t].end);e[t].isBackwards&&(n.cursor=n.start),this.addRange(n,!0)}return}e=e[0]}this.rangeList&&this.toSingleRange(e),this.setSelectionRange(e,e.isBackwards)},this.isEqual=function(e){if((e.length||this.rangeCount)&&e.length!=this.rangeCount)return!1;if(!e.length||!this.ranges)return this.getRange().isEqual(e);for(var t=this.ranges.length;t--;)if(!this.ranges[t].isEqual(e[t]))return!1;return!0}}).call(u.prototype),t.Selection=u}),ace.define("ace/tokenizer",["require","exports","module","ace/config"],function(e,t,n){"use strict";var r=e("./config"),i=2e3,s=function(e){this.states=e,this.regExps={},this.matchMappings={};for(var t in this.states){var n=this.states[t],r=[],i=0,s=this.matchMappings[t]={defaultToken:"text"},o="g",u=[];for(var a=0;a1?f.onMatch=this.$applyToken:f.onMatch=f.token),c>1&&(/\\\d/.test(f.regex)?l=f.regex.replace(/\\([0-9]+)/g,function(e,t){return"\\"+(parseInt(t,10)+i+1)}):(c=1,l=this.removeCapturingGroups(f.regex)),!f.splitRegex&&typeof f.token!="string"&&u.push(f)),s[i]=a,i+=c,r.push(l),f.onMatch||(f.onMatch=null)}r.length||(s[0]=0,r.push("$")),u.forEach(function(e){e.splitRegex=this.createSplitterRegexp(e.regex,o)},this),this.regExps[t]=new RegExp("("+r.join(")|(")+")|($)",o)}};(function(){this.$setMaxTokenCount=function(e){i=e|0},this.$applyToken=function(e){var t=this.splitRegex.exec(e).slice(1),n=this.token.apply(this,t);if(typeof n=="string")return[{type:n,value:e}];var r=[];for(var i=0,s=n.length;il){var g=e.substring(l,m-v.length);h.type==p?h.value+=g:(h.type&&f.push(h),h={type:p,value:g})}for(var y=0;yi){c>2*e.length&&this.reportError("infinite loop with in ace tokenizer",{startState:t,line:e});while(l1&&n[0]!==r&&n.unshift("#tmp",r),{tokens:f,state:n.length?n:r}},this.reportError=r.reportError}).call(s.prototype),t.Tokenizer=s}),ace.define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang"),i=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{defaultToken:"text"}]}};(function(){this.addRules=function(e,t){if(!t){for(var n in e)this.$rules[n]=e[n];return}for(var n in e){var r=e[n];for(var i=0;i=this.$rowTokens.length){this.$row+=1,e||(e=this.$session.getLength());if(this.$row>=e)return this.$row=e-1,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=0}return this.$rowTokens[this.$tokenIndex]},this.getCurrentToken=function(){return this.$rowTokens[this.$tokenIndex]},this.getCurrentTokenRow=function(){return this.$row},this.getCurrentTokenColumn=function(){var e=this.$rowTokens,t=this.$tokenIndex,n=e[t].start;if(n!==undefined)return n;n=0;while(t>0)t-=1,n+=e[t].value.length;return n},this.getCurrentTokenPosition=function(){return{row:this.$row,column:this.getCurrentTokenColumn()}},this.getCurrentTokenRange=function(){var e=this.$rowTokens[this.$tokenIndex],t=this.getCurrentTokenColumn();return new r(this.$row,t,this.$row,t+e.value.length)}}).call(i.prototype),t.TokenIterator=i}),ace.define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","rparen","paren","punctuation.operator"],a=["text","paren.rparen","rparen","paren","punctuation.operator","comment"],f,l={},c={'"':'"',"'":"'"},h=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},p=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},d=function(e){this.add("braces","insertion",function(t,n,r,i,s){var u=r.getCursorPosition(),a=i.doc.getLine(u.row);if(s=="{"){h(r);var l=r.getSelectionRange(),c=i.doc.getTextRange(l);if(c!==""&&c!=="{"&&r.getWrapBehavioursEnabled())return p(l,c,"{","}");if(d.isSaneInsertion(r,i))return/[\]\}\)]/.test(a[u.column])||r.inMultiSelectMode||e&&e.braces?(d.recordAutoInsert(r,i,"}"),{text:"{}",selection:[1,1]}):(d.recordMaybeInsert(r,i,"{"),{text:"{",selection:[1,1]})}else if(s=="}"){h(r);var v=a.substring(u.column,u.column+1);if(v=="}"){var m=i.$findOpeningBracket("}",{column:u.column+1,row:u.row});if(m!==null&&d.isAutoInsertedClosing(u,a,s))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(s=="\n"||s=="\r\n"){h(r);var g="";d.isMaybeInsertedClosing(u,a)&&(g=o.stringRepeat("}",f.maybeInsertedBrackets),d.clearMaybeInsertedClosing());var v=a.substring(u.column,u.column+1);if(v==="}"){var y=i.findMatchingBracket({row:u.row,column:u.column+1},"}");if(!y)return null;var b=this.$getIndent(i.getLine(y.row))}else{if(!g){d.clearMaybeInsertedClosing();return}var b=this.$getIndent(a)}var w=b+i.getTabString();return{text:"\n"+w+"\n"+b+g,selection:[1,w.length,1,w.length]}}d.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"(",")");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"[","]");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){var s=r.$mode.$quotes||c;if(i.length==1&&s[i]){if(this.lineCommentStart&&this.lineCommentStart.indexOf(i)!=-1)return;h(n);var o=i,u=n.getSelectionRange(),a=r.doc.getTextRange(u);if(a!==""&&(a.length!=1||!s[a])&&n.getWrapBehavioursEnabled())return p(u,a,o,o);if(!a){var f=n.getCursorPosition(),l=r.doc.getLine(f.row),d=l.substring(f.column-1,f.column),v=l.substring(f.column,f.column+1),m=r.getTokenAt(f.row,f.column),g=r.getTokenAt(f.row,f.column+1);if(d=="\\"&&m&&/escape/.test(m.type))return null;var y=m&&/string|escape/.test(m.type),b=!g||/string|escape/.test(g.type),w;if(v==o)w=y!==b,w&&/string\.end/.test(g.type)&&(w=!1);else{if(y&&!b)return null;if(y&&b)return null;var E=r.$mode.tokenRe;E.lastIndex=0;var S=E.test(d);E.lastIndex=0;var x=E.test(d);if(S||x)return null;if(v&&!/[\s;,.})\]\\]/.test(v))return null;var T=l[f.column-2];if(!(d!=o||T!=o&&!E.test(T)))return null;w=!0}return{text:w?o+o:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.$mode.$quotes||c,o=r.doc.getTextRange(i);if(!i.isMultiLine()&&s.hasOwnProperty(o)){h(n);var u=r.doc.getLine(i.start.row),a=u.substring(i.start.column+1,i.start.column+2);if(a==o)return i.end.column++,i}})};d.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){if(/[)}\]]/.test(e.session.getLine(n.row)[n.column]))return!0;var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},d.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},d.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},d.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},d.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},d.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},d.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},d.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(d,i),t.CstyleBehaviour=d}),ace.define("ace/unicode",["require","exports","module"],function(e,t,n){"use strict";var r=[48,9,8,25,5,0,2,25,48,0,11,0,5,0,6,22,2,30,2,457,5,11,15,4,8,0,2,0,18,116,2,1,3,3,9,0,2,2,2,0,2,19,2,82,2,138,2,4,3,155,12,37,3,0,8,38,10,44,2,0,2,1,2,1,2,0,9,26,6,2,30,10,7,61,2,9,5,101,2,7,3,9,2,18,3,0,17,58,3,100,15,53,5,0,6,45,211,57,3,18,2,5,3,11,3,9,2,1,7,6,2,2,2,7,3,1,3,21,2,6,2,0,4,3,3,8,3,1,3,3,9,0,5,1,2,4,3,11,16,2,2,5,5,1,3,21,2,6,2,1,2,1,2,1,3,0,2,4,5,1,3,2,4,0,8,3,2,0,8,15,12,2,2,8,2,2,2,21,2,6,2,1,2,4,3,9,2,2,2,2,3,0,16,3,3,9,18,2,2,7,3,1,3,21,2,6,2,1,2,4,3,8,3,1,3,2,9,1,5,1,2,4,3,9,2,0,17,1,2,5,4,2,2,3,4,1,2,0,2,1,4,1,4,2,4,11,5,4,4,2,2,3,3,0,7,0,15,9,18,2,2,7,2,2,2,22,2,9,2,4,4,7,2,2,2,3,8,1,2,1,7,3,3,9,19,1,2,7,2,2,2,22,2,9,2,4,3,8,2,2,2,3,8,1,8,0,2,3,3,9,19,1,2,7,2,2,2,22,2,15,4,7,2,2,2,3,10,0,9,3,3,9,11,5,3,1,2,17,4,23,2,8,2,0,3,6,4,0,5,5,2,0,2,7,19,1,14,57,6,14,2,9,40,1,2,0,3,1,2,0,3,0,7,3,2,6,2,2,2,0,2,0,3,1,2,12,2,2,3,4,2,0,2,5,3,9,3,1,35,0,24,1,7,9,12,0,2,0,2,0,5,9,2,35,5,19,2,5,5,7,2,35,10,0,58,73,7,77,3,37,11,42,2,0,4,328,2,3,3,6,2,0,2,3,3,40,2,3,3,32,2,3,3,6,2,0,2,3,3,14,2,56,2,3,3,66,5,0,33,15,17,84,13,619,3,16,2,25,6,74,22,12,2,6,12,20,12,19,13,12,2,2,2,1,13,51,3,29,4,0,5,1,3,9,34,2,3,9,7,87,9,42,6,69,11,28,4,11,5,11,11,39,3,4,12,43,5,25,7,10,38,27,5,62,2,28,3,10,7,9,14,0,89,75,5,9,18,8,13,42,4,11,71,55,9,9,4,48,83,2,2,30,14,230,23,280,3,5,3,37,3,5,3,7,2,0,2,0,2,0,2,30,3,52,2,6,2,0,4,2,2,6,4,3,3,5,5,12,6,2,2,6,67,1,20,0,29,0,14,0,17,4,60,12,5,0,4,11,18,0,5,0,3,9,2,0,4,4,7,0,2,0,2,0,2,3,2,10,3,3,6,4,5,0,53,1,2684,46,2,46,2,132,7,6,15,37,11,53,10,0,17,22,10,6,2,6,2,6,2,6,2,6,2,6,2,6,2,6,2,31,48,0,470,1,36,5,2,4,6,1,5,85,3,1,3,2,2,89,2,3,6,40,4,93,18,23,57,15,513,6581,75,20939,53,1164,68,45,3,268,4,27,21,31,3,13,13,1,2,24,9,69,11,1,38,8,3,102,3,1,111,44,25,51,13,68,12,9,7,23,4,0,5,45,3,35,13,28,4,64,15,10,39,54,10,13,3,9,7,22,4,1,5,66,25,2,227,42,2,1,3,9,7,11171,13,22,5,48,8453,301,3,61,3,105,39,6,13,4,6,11,2,12,2,4,2,0,2,1,2,1,2,107,34,362,19,63,3,53,41,11,5,15,17,6,13,1,25,2,33,4,2,134,20,9,8,25,5,0,2,25,12,88,4,5,3,5,3,5,3,2],i=0,s=[];for(var o=0;o2?r%f!=f-1:r%f==0}}var E=Infinity;w(function(e,t){var n=e.search(/\S/);n!==-1?(ne.length&&(E=e.length)}),u==Infinity&&(u=E,s=!1,o=!1),l&&u%f!=0&&(u=Math.floor(u/f)*f),w(o?m:v)},this.toggleBlockComment=function(e,t,n,r){var i=this.blockComment;if(!i)return;!i.start&&i[0]&&(i=i[0]);var s=new f(t,r.row,r.column),o=s.getCurrentToken(),u=t.selection,a=t.selection.toOrientedRange(),c,h;if(o&&/comment/.test(o.type)){var p,d;while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.start);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;p=new l(m,g,m,g+i.start.length);break}o=s.stepBackward()}var s=new f(t,r.row,r.column),o=s.getCurrentToken();while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.end);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;d=new l(m,g,m,g+i.end.length);break}o=s.stepForward()}d&&t.remove(d),p&&(t.remove(p),c=p.start.row,h=-i.start.length)}else h=i.start.length,c=n.start.row,t.insert(n.end,i.end),t.insert(n.start,i.start);a.start.row==c&&(a.start.column+=h),a.end.row==c&&(a.end.column+=h),t.selection.fromOrientedRange(a)},this.getNextLineIndent=function(e,t,n){return this.$getIndent(t)},this.checkOutdent=function(e,t,n){return!1},this.autoOutdent=function(e,t,n){},this.$getIndent=function(e){return e.match(/^\s*/)[0]},this.createWorker=function(e){return null},this.createModeDelegates=function(e){this.$embeds=[],this.$modes={};for(var t in e)if(e[t]){var n=e[t],i=n.prototype.$id,s=r.$modes[i];s||(r.$modes[i]=s=new n),r.$modes[t]||(r.$modes[t]=s),this.$embeds.push(t),this.$modes[t]=s}var o=["toggleBlockComment","toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction","getCompletions"];for(var t=0;t=0&&t.row=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),ace.define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=t.Anchor=function(e,t,n){this.$onChange=this.onChange.bind(this),this.attach(e),typeof n=="undefined"?this.setPosition(t.row,t.column):this.setPosition(t,n)};(function(){function e(e,t,n){var r=n?e.column<=t.column:e.columnthis.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.off("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e||"")},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e0,r=t=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4?this.$splitAndapplyLargeDelta(e,2e4):(i(this.$lines,e,t),this._signal("change",e))},this.$safeApplyDelta=function(e){var t=this.$lines.length;(e.action=="remove"&&e.start.row20){n.running=setTimeout(n.$worker,20);break}}n.currentLine=t,r==-1&&(r=t),s<=r&&n.fireUpdateEvent(s,r)}};(function(){r.implement(this,i),this.setTokenizer=function(e){this.tokenizer=e,this.lines=[],this.states=[],this.start(0)},this.setDocument=function(e){this.doc=e,this.lines=[],this.states=[],this.stop()},this.fireUpdateEvent=function(e,t){var n={first:e,last:t};this._signal("update",{data:n})},this.start=function(e){this.currentLine=Math.min(e||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.states.splice(this.currentLine,this.states.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.scheduleStart=function(){this.running||(this.running=setTimeout(this.$worker,700))},this.$updateOnChange=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.lines[t]=null;else if(e.action=="remove")this.lines.splice(t,n+1,null),this.states.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.lines.splice.apply(this.lines,r),this.states.splice.apply(this.states,r)}this.currentLine=Math.min(t,this.currentLine,this.doc.getLength()),this.stop()},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(e){return this.lines[e]||this.$tokenizeRow(e)},this.getState=function(e){return this.currentLine==e&&this.$tokenizeRow(e),this.states[e]||"start"},this.$tokenizeRow=function(e){var t=this.doc.getLine(e),n=this.states[e-1],r=this.tokenizer.getLineTokens(t,n,e);return this.states[e]+""!=r.state+""?(this.states[e]=r.state,this.lines[e+1]=null,this.currentLine>e+1&&(this.currentLine=e+1)):this.currentLine==e&&(this.currentLine=e+1),this.lines[e]=r.tokens},this.cleanup=function(){this.running=!1,this.lines=[],this.states=[],this.currentLine=0,this.removeAllListeners()}}).call(s.prototype),t.BackgroundTokenizer=s}),ace.define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(e,t,n){this.setRegexp(e),this.clazz=t,this.type=n||"text"};(function(){this.MAX_RANGES=500,this.setRegexp=function(e){if(this.regExp+""==e+"")return;this.regExp=e,this.cache=[]},this.update=function(e,t,n,i){if(!this.regExp)return;var o=i.firstRow,u=i.lastRow,a={};for(var f=o;f<=u;f++){var l=this.cache[f];l==null&&(l=r.getMatchOffsets(n.getLine(f),this.regExp),l.length>this.MAX_RANGES&&(l=l.slice(0,this.MAX_RANGES)),l=l.map(function(e){return new s(f,e.offset,f,e.offset+e.length)}),this.cache[f]=l.length?l:"");for(var c=l.length;c--;){var h=l[c].toScreenRange(n),p=h.toString();if(a[p])continue;a[p]=!0,t.drawSingleLineMarker(e,h,this.clazz,i)}}}}).call(o.prototype),t.SearchHighlight=o}),ace.define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){this.foldData=e,Array.isArray(t)?this.folds=t:t=this.folds=[t];var n=t[t.length-1];this.range=new r(t[0].start.row,t[0].start.column,n.end.row,n.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(e){e.setFoldLine(this)},this)}var r=e("../range").Range;(function(){this.shiftRow=function(e){this.start.row+=e,this.end.row+=e,this.folds.forEach(function(t){t.start.row+=e,t.end.row+=e})},this.addFold=function(e){if(e.sameRow){if(e.start.rowthis.endRow)throw new Error("Can't add a fold to this FoldLine as it has no connection");this.folds.push(e),this.folds.sort(function(e,t){return-e.range.compareEnd(t.start.row,t.start.column)}),this.range.compareEnd(e.start.row,e.start.column)>0?(this.end.row=e.end.row,this.end.column=e.end.column):this.range.compareStart(e.end.row,e.end.column)<0&&(this.start.row=e.start.row,this.start.column=e.start.column)}else if(e.start.row==this.end.row)this.folds.push(e),this.end.row=e.end.row,this.end.column=e.end.column;else{if(e.end.row!=this.start.row)throw new Error("Trying to add fold to FoldRow that doesn't have a matching row");this.folds.unshift(e),this.start.row=e.start.row,this.start.column=e.start.column}e.foldLine=this},this.containsRow=function(e){return e>=this.start.row&&e<=this.end.row},this.walk=function(e,t,n){var r=0,i=this.folds,s,o,u,a=!0;t==null&&(t=this.end.row,n=this.end.column);for(var f=0;f0)continue;var a=i(e,o.start);return u===0?t&&a!==0?-s-2:s:a>0||a===0&&!t?s:-s-1}return-s-1},this.add=function(e){var t=!e.isEmpty(),n=this.pointIndex(e.start,t);n<0&&(n=-n-1);var r=this.pointIndex(e.end,t,n);return r<0?r=-r-1:r++,this.ranges.splice(n,r-n,e)},this.addList=function(e){var t=[];for(var n=e.length;n--;)t.push.apply(t,this.add(e[n]));return t},this.substractPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges.splice(t,1)},this.merge=function(){var e=[],t=this.ranges;t=t.sort(function(e,t){return i(e.start,t.start)});var n=t[0],r;for(var s=1;s=0},this.containsPoint=function(e){return this.pointIndex(e)>=0},this.rangeAtPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges[t]},this.clipRows=function(e,t){var n=this.ranges;if(n[0].start.row>t||n[n.length-1].start.row=r)break}if(e.action=="insert"){var f=i-r,l=-t.column+n.column;for(;or)break;a.start.row==r&&a.start.column>=t.column&&(a.start.column==t.column&&this.$bias<=0||(a.start.column+=l,a.start.row+=f));if(a.end.row==r&&a.end.column>=t.column){if(a.end.column==t.column&&this.$bias<0)continue;a.end.column==t.column&&l>0&&oa.start.column&&a.end.column==s[o+1].start.column&&(a.end.column-=l),a.end.column+=l,a.end.row+=f}}}else{var f=r-i,l=t.column-n.column;for(;oi)break;if(a.end.rowt.column)a.end.column=t.column,a.end.row=t.row}else a.end.column+=l,a.end.row+=f;else a.end.row>i&&(a.end.row+=f);if(a.start.rowt.column)a.start.column=t.column,a.start.row=t.row}else a.start.column+=l,a.start.row+=f;else a.start.row>i&&(a.start.row+=f)}}if(f!=0&&o=e)return i;if(i.end.row>e)return null}return null},this.getNextFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r=e)return i}return null},this.getFoldedRowCount=function(e,t){var n=this.$foldData,r=t-e+1;for(var i=0;i=t){u=e?r-=t-u:r=0);break}o>=e&&(u>=e?r-=o-u:r-=o-e+1)}return r},this.$addFoldLine=function(e){return this.$foldData.push(e),this.$foldData.sort(function(e,t){return e.start.row-t.start.row}),e},this.addFold=function(e,t){var n=this.$foldData,r=!1,o;e instanceof s?o=e:(o=new s(t,e),o.collapseChildren=t.collapseChildren),this.$clipRangeToDocument(o.range);var u=o.start.row,a=o.start.column,f=o.end.row,l=o.end.column,c=this.getFoldAt(u,a,1),h=this.getFoldAt(f,l,-1);if(c&&h==c)return c.addSubFold(o);c&&!c.range.isStart(u,a)&&this.removeFold(c),h&&!h.range.isEnd(f,l)&&this.removeFold(h);var p=this.getFoldsInRange(o.range);p.length>0&&(this.removeFolds(p),o.collapseChildren||p.forEach(function(e){o.addSubFold(e)}));for(var d=0;d0&&this.foldAll(e.start.row+1,e.end.row,e.collapseChildren-1),e.subFolds=[]},this.expandFolds=function(e){e.forEach(function(e){this.expandFold(e)},this)},this.unfold=function(e,t){var n,i;if(e==null)n=new r(0,0,this.getLength(),0),t==null&&(t=!0);else if(typeof e=="number")n=new r(e,0,e,this.getLine(e).length);else if("row"in e)n=r.fromPoints(e,e);else{if(Array.isArray(e))return i=[],e.forEach(function(e){i=i.concat(this.unfold(e))},this),i;n=e}i=this.getFoldsInRangeList(n);var s=i;while(i.length==1&&r.comparePoints(i[0].start,n.start)<0&&r.comparePoints(i[0].end,n.end)>0)this.expandFolds(i),i=this.getFoldsInRangeList(n);t!=0?this.removeFolds(i):this.expandFolds(i);if(s.length)return s},this.isRowFolded=function(e,t){return!!this.getFoldLine(e,t)},this.getRowFoldEnd=function(e,t){var n=this.getFoldLine(e,t);return n?n.end.row:e},this.getRowFoldStart=function(e,t){var n=this.getFoldLine(e,t);return n?n.start.row:e},this.getFoldDisplayLine=function(e,t,n,r,i){r==null&&(r=e.start.row),i==null&&(i=0),t==null&&(t=e.end.row),n==null&&(n=this.getLine(t).length);var s=this.doc,o="";return e.walk(function(e,t,n,u){if(tl)break}while(s&&a.test(s.type)&&!/^comment.start/.test(s.type));s=i.stepBackward()}else s=i.getCurrentToken();return f.end.row=i.getCurrentTokenRow(),f.end.column=i.getCurrentTokenColumn(),/^comment.end/.test(s.type)||(f.end.column+=s.value.length-2),f}},this.foldAll=function(e,t,n,r){n==undefined&&(n=1e5);var i=this.foldWidgets;if(!i)return;t=t||this.getLength(),e=e||0;for(var s=e;s=e&&(s=o.end.row,o.collapseChildren=n,this.addFold("...",o))}},this.foldToLevel=function(e){this.foldAll();while(e-->0)this.unfold(null,!1)},this.foldAllComments=function(){var e=this;this.foldAll(null,null,null,function(t){var n=e.getTokens(t);for(var r=0;r=0){var s=n[r];s==null&&(s=n[r]=this.getFoldWidget(r));if(s=="start"){var o=this.getFoldWidgetRange(r);i||(i=o);if(o&&o.end.row>=e)break}r--}return{range:r!==-1&&o,firstRange:i}},this.onFoldWidgetClick=function(e,t){t=t.domEvent;var n={children:t.shiftKey,all:t.ctrlKey||t.metaKey,siblings:t.altKey},r=this.$toggleFoldWidget(e,n);if(!r){var i=t.target||t.srcElement;i&&/ace_fold-widget/.test(i.className)&&(i.className+=" ace_invalid")}},this.$toggleFoldWidget=function(e,t){if(!this.getFoldWidget)return;var n=this.getFoldWidget(e),r=this.getLine(e),i=n==="end"?-1:1,s=this.getFoldAt(e,i===-1?0:r.length,i);if(s)return t.children||t.all?this.removeFold(s):this.expandFold(s),s;var o=this.getFoldWidgetRange(e,!0);if(o&&!o.isMultiLine()){s=this.getFoldAt(o.start.row,o.start.column,1);if(s&&o.isEqual(s.range))return this.removeFold(s),s}if(t.siblings){var u=this.getParentFoldRangeData(e);if(u.range)var a=u.range.start.row+1,f=u.range.end.row;this.foldAll(a,f,t.all?1e4:0)}else t.children?(f=o?o.end.row:this.getLength(),this.foldAll(e+1,f,t.all?1e4:0)):o&&(t.all&&(o.collapseChildren=1e4),this.addFold("...",o));return o},this.toggleFoldWidget=function(e){var t=this.selection.getCursor().row;t=this.getRowFoldStart(t);var n=this.$toggleFoldWidget(t,{});if(n)return;var r=this.getParentFoldRangeData(t,!0);n=r.range||r.firstRange;if(n){t=n.start.row;var i=this.getFoldAt(t,this.getLine(t).length,1);i?this.removeFold(i):this.addFold("...",n)}},this.updateFoldWidgets=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.foldWidgets[t]=null;else if(e.action=="remove")this.foldWidgets.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.foldWidgets.splice.apply(this.foldWidgets,r)}},this.tokenizerUpdateFoldWidgets=function(e){var t=e.data;t.first!=t.last&&this.foldWidgets.length>t.first&&this.foldWidgets.splice(t.first,this.foldWidgets.length)}}var r=e("../range").Range,i=e("./fold_line").FoldLine,s=e("./fold").Fold,o=e("../token_iterator").TokenIterator;t.Folding=u}),ace.define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"],function(e,t,n){"use strict";function s(){this.findMatchingBracket=function(e,t){if(e.column==0)return null;var n=t||this.getLine(e.row).charAt(e.column-1);if(n=="")return null;var r=n.match(/([\(\[\{])|([\)\]\}])/);return r?r[1]?this.$findClosingBracket(r[1],e):this.$findOpeningBracket(r[2],e):null},this.getBracketRange=function(e){var t=this.getLine(e.row),n=!0,r,s=t.charAt(e.column-1),o=s&&s.match(/([\(\[\{])|([\)\]\}])/);o||(s=t.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(/([\(\[\{])|([\)\]\}])/),n=!1);if(!o)return null;if(o[1]){var u=this.$findClosingBracket(o[1],e);if(!u)return null;r=i.fromPoints(e,u),n||(r.end.column++,r.start.column--),r.cursor=r.end}else{var u=this.$findOpeningBracket(o[2],e);if(!u)return null;r=i.fromPoints(u,e),n||(r.start.column++,r.end.column--),r.cursor=r.start}return r},this.getMatchingBracketRanges=function(e,t){var n=this.getLine(e.row),r=/([\(\[\{])|([\)\]\}])/,s=!t&&n.charAt(e.column-1),o=s&&s.match(r);o||(s=(t===undefined||t)&&n.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(r));if(!o)return null;var u=new i(e.row,e.column-1,e.row,e.column),a=o[1]?this.$findClosingBracket(o[1],e):this.$findOpeningBracket(o[2],e);if(!a)return[u];var f=new i(a.row,a.column,a.row,a.column+1);return[u,f]},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{","<":">",">":"<"},this.$findOpeningBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("rparen",".paren").replace(/\b(?:end)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn()-2,f=u.value;for(;;){while(a>=0){var l=f.charAt(a);if(l==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else l==e&&(s+=1);a-=1}do u=o.stepBackward();while(u&&!n.test(u.type));if(u==null)break;f=u.value,a=f.length-1}return null},this.$findClosingBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("lparen",".paren").replace(/\b(?:start|begin)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn();for(;;){var f=u.value,l=f.length;while(a"?r=!0:t.type.indexOf("tag-name")!==-1&&(n=!0));while(t&&!n);return t},this.$findClosingTag=function(e,t){var n,r=t.value,s=t.value,o=0,u=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1);t=e.stepForward();var a=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+t.value.length),f=!1;do{n=t,t=e.stepForward();if(t){if(t.value===">"&&!f){var l=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1);f=!0}if(t.type.indexOf("tag-name")!==-1){r=t.value;if(s===r)if(n.value==="<")o++;else if(n.value==="")return;var p=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1)}}}else if(s===r&&t.value==="/>"){o--;if(o<0)var c=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+2),h=c,p=h,l=new i(a.end.row,a.end.column,a.end.row,a.end.column+1)}}}while(t&&o>=0);if(u&&l&&c&&p&&a&&h)return{openTag:new i(u.start.row,u.start.column,l.end.row,l.end.column),closeTag:new i(c.start.row,c.start.column,p.end.row,p.end.column),openTagName:a,closeTagName:h}},this.$findOpeningTag=function(e,t){var n=e.getCurrentToken(),r=t.value,s=0,o=e.getCurrentTokenRow(),u=e.getCurrentTokenColumn(),a=u+2,f=new i(o,u,o,a);e.stepForward();var l=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+t.value.length);t=e.stepForward();if(!t||t.value!==">")return;var c=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1);e.stepBackward(),e.stepBackward();do{t=n,o=e.getCurrentTokenRow(),u=e.getCurrentTokenColumn(),a=u+t.value.length,n=e.stepBackward();if(t)if(t.type.indexOf("tag-name")!==-1){if(r===t.value)if(n.value==="<"){s++;if(s>0){var h=new i(o,u,o,a),p=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1);do t=e.stepForward();while(t&&t.value!==">");var d=new i(e.getCurrentTokenRow(),e.getCurrentTokenColumn(),e.getCurrentTokenRow(),e.getCurrentTokenColumn()+1)}}else n.value===""){var v=0,m=n;while(m){if(m.type.indexOf("tag-name")!==-1&&m.value===r){s--;break}if(m.value==="<")break;m=e.stepBackward(),v++}for(var g=0;g=4352&&e<=4447||e>=4515&&e<=4519||e>=4602&&e<=4607||e>=9001&&e<=9002||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12283||e>=12288&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12589||e>=12593&&e<=12686||e>=12688&&e<=12730||e>=12736&&e<=12771||e>=12784&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=13054||e>=13056&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=55216&&e<=55238||e>=55243&&e<=55291||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=65281&&e<=65376||e>=65504&&e<=65510}r.implement(this,u),this.setDocument=function(e){this.doc&&this.doc.off("change",this.$onChange),this.doc=e,e.on("change",this.$onChange,!0),this.bgTokenizer.setDocument(this.getDocument()),this.resetCaches()},this.getDocument=function(){return this.doc},this.$resetRowCache=function(e){if(!e){this.$docRowCache=[],this.$screenRowCache=[];return}var t=this.$docRowCache.length,n=this.$getRowCacheIndex(this.$docRowCache,e)+1;t>n&&(this.$docRowCache.splice(n,t),this.$screenRowCache.splice(n,t))},this.$getRowCacheIndex=function(e,t){var n=0,r=e.length-1;while(n<=r){var i=n+r>>1,s=e[i];if(t>s)n=i+1;else{if(!(t=t)break}return r=n[s],r?(r.index=s,r.start=i-r.value.length,r):null},this.setUndoManager=function(e){this.$undoManager=e,this.$informUndoManager&&this.$informUndoManager.cancel();if(e){var t=this;e.addSession(this),this.$syncInformUndoManager=function(){t.$informUndoManager.cancel(),t.mergeUndoDeltas=!1},this.$informUndoManager=i.delayedCall(this.$syncInformUndoManager)}else this.$syncInformUndoManager=function(){}},this.markUndoGroup=function(){this.$syncInformUndoManager&&this.$syncInformUndoManager()},this.$defaultUndoManager={undo:function(){},redo:function(){},hasUndo:function(){},hasRedo:function(){},reset:function(){},add:function(){},addSelection:function(){},startNewGroup:function(){},addSession:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?i.stringRepeat(" ",this.getTabSize()):" "},this.setUseSoftTabs=function(e){this.setOption("useSoftTabs",e)},this.getUseSoftTabs=function(){return this.$useSoftTabs&&!this.$mode.$indentWithTabs},this.setTabSize=function(e){this.setOption("tabSize",e)},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(e){return this.$useSoftTabs&&e.column%this.$tabSize===0},this.setNavigateWithinSoftTabs=function(e){this.setOption("navigateWithinSoftTabs",e)},this.getNavigateWithinSoftTabs=function(){return this.$navigateWithinSoftTabs},this.$overwrite=!1,this.setOverwrite=function(e){this.setOption("overwrite",e)},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.addGutterDecoration=function(e,t){this.$decorations[e]||(this.$decorations[e]=""),this.$decorations[e]+=" "+t,this._signal("changeBreakpoint",{})},this.removeGutterDecoration=function(e,t){this.$decorations[e]=(this.$decorations[e]||"").replace(" "+t,""),this._signal("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(e){this.$breakpoints=[];for(var t=0;t0&&(r=!!n.charAt(t-1).match(this.tokenRe)),r||(r=!!n.charAt(t).match(this.tokenRe));if(r)var i=this.tokenRe;else if(/^\s+$/.test(n.slice(t-1,t+1)))var i=/\s/;else var i=this.nonTokenRe;var s=t;if(s>0){do s--;while(s>=0&&n.charAt(s).match(i));s++}var o=t;while(oe&&(e=t.screenWidth)}),this.lineWidgetWidth=e},this.$computeWidth=function(e){if(this.$modified||e){this.$modified=!1;if(this.$useWrapMode)return this.screenWidth=this.$wrapLimit;var t=this.doc.getAllLines(),n=this.$rowLengthCache,r=0,i=0,s=this.$foldData[i],o=s?s.start.row:Infinity,u=t.length;for(var a=0;ao){a=s.end.row+1;if(a>=u)break;s=this.$foldData[i++],o=s?s.start.row:Infinity}n[a]==null&&(n[a]=this.$getStringScreenWidth(t[a])[0]),n[a]>r&&(r=n[a])}this.screenWidth=r}},this.getLine=function(e){return this.doc.getLine(e)},this.getLines=function(e,t){return this.doc.getLines(e,t)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(e){return this.doc.getTextRange(e||this.selection.getRange())},this.insert=function(e,t){return this.doc.insert(e,t)},this.remove=function(e){return this.doc.remove(e)},this.removeFullLines=function(e,t){return this.doc.removeFullLines(e,t)},this.undoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=e.length-1;n!=-1;n--){var r=e[n];r.action=="insert"||r.action=="remove"?this.doc.revertDelta(r):r.folds&&this.addFolds(r.folds)}!t&&this.$undoSelect&&(e.selectionBefore?this.selection.fromJSON(e.selectionBefore):this.selection.setRange(this.$getUndoSelection(e,!0))),this.$fromUndo=!1},this.redoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=0;ne.end.column&&(s.start.column+=u),s.end.row==e.end.row&&s.end.column>e.end.column&&(s.end.column+=u)),o&&s.start.row>=e.end.row&&(s.start.row+=o,s.end.row+=o)}s.end=this.insert(s.start,r);if(i.length){var a=e.start,f=s.start,o=f.row-a.row,u=f.column-a.column;this.addFolds(i.map(function(e){return e=e.clone(),e.start.row==a.row&&(e.start.column+=u),e.end.row==a.row&&(e.end.column+=u),e.start.row+=o,e.end.row+=o,e}))}return s},this.indentRows=function(e,t,n){n=n.replace(/\t/g,this.getTabString());for(var r=e;r<=t;r++)this.doc.insertInLine({row:r,column:0},n)},this.outdentRows=function(e){var t=e.collapseRows(),n=new l(0,0,0,0),r=this.getTabSize();for(var i=t.start.row;i<=t.end.row;++i){var s=this.getLine(i);n.start.row=i,n.end.row=i;for(var o=0;o0){var r=this.getRowFoldEnd(t+n);if(r>this.doc.getLength()-1)return 0;var i=r-t}else{e=this.$clipRowToDocument(e),t=this.$clipRowToDocument(t);var i=t-e+1}var s=new l(e,0,t,Number.MAX_VALUE),o=this.getFoldsInRange(s).map(function(e){return e=e.clone(),e.start.row+=i,e.end.row+=i,e}),u=n==0?this.doc.getLines(e,t):this.doc.removeFullLines(e,t);return this.doc.insertFullLines(e+i,u),o.length&&this.addFolds(o),i},this.moveLinesUp=function(e,t){return this.$moveLines(e,t,-1)},this.moveLinesDown=function(e,t){return this.$moveLines(e,t,1)},this.duplicateLines=function(e,t){return this.$moveLines(e,t,0)},this.$clipRowToDocument=function(e){return Math.max(0,Math.min(e,this.doc.getLength()-1))},this.$clipColumnToRow=function(e,t){return t<0?0:Math.min(this.doc.getLine(e).length,t)},this.$clipPositionToDocument=function(e,t){t=Math.max(0,t);if(e<0)e=0,t=0;else{var n=this.doc.getLength();e>=n?(e=n-1,t=this.doc.getLine(n-1).length):t=Math.min(this.doc.getLine(e).length,t)}return{row:e,column:t}},this.$clipRangeToDocument=function(e){e.start.row<0?(e.start.row=0,e.start.column=0):e.start.column=this.$clipColumnToRow(e.start.row,e.start.column);var t=this.doc.getLength()-1;return e.end.row>t?(e.end.row=t,e.end.column=this.doc.getLine(t).length):e.end.column=this.$clipColumnToRow(e.end.row,e.end.column),e},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(e){if(e!=this.$useWrapMode){this.$useWrapMode=e,this.$modified=!0,this.$resetRowCache(0);if(e){var t=this.getLength();this.$wrapData=Array(t),this.$updateWrapData(0,t-1)}this._signal("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(e,t){if(this.$wrapLimitRange.min!==e||this.$wrapLimitRange.max!==t)this.$wrapLimitRange={min:e,max:t},this.$modified=!0,this.$bidiHandler.markAsDirty(),this.$useWrapMode&&this._signal("changeWrapMode")},this.adjustWrapLimit=function(e,t){var n=this.$wrapLimitRange;n.max<0&&(n={min:t,max:t});var r=this.$constrainWrapLimit(e,n.min,n.max);return r!=this.$wrapLimit&&r>1?(this.$wrapLimit=r,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._signal("changeWrapLimit")),!0):!1},this.$constrainWrapLimit=function(e,t,n){return t&&(e=Math.max(t,e)),n&&(e=Math.min(n,e)),e},this.getWrapLimit=function(){return this.$wrapLimit},this.setWrapLimit=function(e){this.setWrapLimitRange(e,e)},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(e){var t=this.$useWrapMode,n=e.action,r=e.start,i=e.end,s=r.row,o=i.row,u=o-s,a=null;this.$updating=!0;if(u!=0)if(n==="remove"){this[t?"$wrapData":"$rowLengthCache"].splice(s,u);var f=this.$foldData;a=this.getFoldsInRange(e),this.removeFolds(a);var l=this.getFoldLine(i.row),c=0;if(l){l.addRemoveChars(i.row,i.column,r.column-i.column),l.shiftRow(-u);var h=this.getFoldLine(s);h&&h!==l&&(h.merge(l),l=h),c=f.indexOf(l)+1}for(c;c=i.row&&l.shiftRow(-u)}o=s}else{var p=Array(u);p.unshift(s,0);var d=t?this.$wrapData:this.$rowLengthCache;d.splice.apply(d,p);var f=this.$foldData,l=this.getFoldLine(s),c=0;if(l){var v=l.range.compareInside(r.row,r.column);v==0?(l=l.split(r.row,r.column),l&&(l.shiftRow(u),l.addRemoveChars(o,0,i.column-r.column))):v==-1&&(l.addRemoveChars(s,0,i.column-r.column),l.shiftRow(u)),c=f.indexOf(l)+1}for(c;c=s&&l.shiftRow(u)}}else{u=Math.abs(e.start.column-e.end.column),n==="remove"&&(a=this.getFoldsInRange(e),this.removeFolds(a),u=-u);var l=this.getFoldLine(s);l&&l.addRemoveChars(s,r.column,u)}return t&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),this.$updating=!1,t?this.$updateWrapData(s,o):this.$updateRowLengthCache(s,o),a},this.$updateRowLengthCache=function(e,t,n){this.$rowLengthCache[e]=null,this.$rowLengthCache[t]=null},this.$updateWrapData=function(e,t){var r=this.doc.getAllLines(),i=this.getTabSize(),o=this.$wrapData,u=this.$wrapLimit,a,f,l=e;t=Math.min(t,r.length-1);while(l<=t)f=this.getFoldLine(l,f),f?(a=[],f.walk(function(e,t,i,o){var u;if(e!=null){u=this.$getDisplayTokens(e,a.length),u[0]=n;for(var f=1;fr-b){var w=f+r-b;if(e[w-1]>=c&&e[w]>=c){y(w);continue}if(e[w]==n||e[w]==s){for(w;w!=f-1;w--)if(e[w]==n)break;if(w>f){y(w);continue}w=f+r;for(w;w>2)),f-1);while(w>E&&e[w]E&&e[w]E&&e[w]==a)w--}else while(w>E&&e[w]E){y(++w);continue}w=f+r,e[w]==t&&w--,y(w-b)}return o},this.$getDisplayTokens=function(n,r){var i=[],s;r=r||0;for(var o=0;o39&&u<48||u>57&&u<64?i.push(a):u>=4352&&v(u)?i.push(e,t):i.push(e)}return i},this.$getStringScreenWidth=function(e,t,n){if(t==0)return[0,0];t==null&&(t=Infinity),n=n||0;var r,i;for(i=0;i=4352&&v(r)?n+=2:n+=1;if(n>t)break}return[n,i]},this.lineWidgets=null,this.getRowLength=function(e){var t=1;return this.lineWidgets&&(t+=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0),!this.$useWrapMode||!this.$wrapData[e]?t:this.$wrapData[e].length+t},this.getRowLineCount=function(e){return!this.$useWrapMode||!this.$wrapData[e]?1:this.$wrapData[e].length+1},this.getRowWrapIndent=function(e){if(this.$useWrapMode){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE),n=this.$wrapData[t.row];return n.length&&n[0]=0)var u=f[l],i=this.$docRowCache[l],h=e>f[c-1];else var h=!c;var p=this.getLength()-1,d=this.getNextFoldLine(i),v=d?d.start.row:Infinity;while(u<=e){a=this.getRowLength(i);if(u+a>e||i>=p)break;u+=a,i++,i>v&&(i=d.end.row+1,d=this.getNextFoldLine(i,d),v=d?d.start.row:Infinity),h&&(this.$docRowCache.push(i),this.$screenRowCache.push(u))}if(d&&d.start.row<=i)r=this.getFoldDisplayLine(d),i=d.start.row;else{if(u+a<=e||i>p)return{row:p,column:this.getLine(p).length};r=this.getLine(i),d=null}var m=0,g=Math.floor(e-u);if(this.$useWrapMode){var y=this.$wrapData[i];y&&(o=y[g],g>0&&y.length&&(m=y.indent,s=y[g-1]||y[y.length-1],r=r.substring(s)))}return n!==undefined&&this.$bidiHandler.isBidiRow(u+g,i,g)&&(t=this.$bidiHandler.offsetToCol(n)),s+=this.$getStringScreenWidth(r,t-m)[1],this.$useWrapMode&&s>=o&&(s=o-1),d?d.idxToPosition(s):{row:i,column:s}},this.documentToScreenPosition=function(e,t){if(typeof t=="undefined")var n=this.$clipPositionToDocument(e.row,e.column);else n=this.$clipPositionToDocument(e,t);e=n.row,t=n.column;var r=0,i=null,s=null;s=this.getFoldAt(e,t,1),s&&(e=s.start.row,t=s.start.column);var o,u=0,a=this.$docRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var u=a[f],r=this.$screenRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getNextFoldLine(u),p=h?h.start.row:Infinity;while(u=p){o=h.end.row+1;if(o>e)break;h=this.getNextFoldLine(o,h),p=h?h.start.row:Infinity}else o=u+1;r+=this.getRowLength(u),u=o,c&&(this.$docRowCache.push(u),this.$screenRowCache.push(r))}var d="";h&&u>=p?(d=this.getFoldDisplayLine(h,e,t),i=h.start.row):(d=this.getLine(e).substring(0,t),i=e);var v=0;if(this.$useWrapMode){var m=this.$wrapData[i];if(m){var g=0;while(d.length>=m[g])r++,g++;d=d.substring(m[g-1]||0,d.length),v=g>0?m.indent:0}}return this.lineWidgets&&this.lineWidgets[u]&&this.lineWidgets[u].rowsAbove&&(r+=this.lineWidgets[u].rowsAbove),{row:r,column:v+this.$getStringScreenWidth(d)[0]}},this.documentToScreenColumn=function(e,t){return this.documentToScreenPosition(e,t).column},this.documentToScreenRow=function(e,t){return this.documentToScreenPosition(e,t).row},this.getScreenLength=function(){var e=0,t=null;if(!this.$useWrapMode){e=this.getLength();var n=this.$foldData;for(var r=0;ro&&(s=t.end.row+1,t=this.$foldData[r++],o=t?t.start.row:Infinity)}}return this.lineWidgets&&(e+=this.$getWidgetScreenLength()),e},this.$setFontMetrics=function(e){if(!this.$enableVarChar)return;this.$getStringScreenWidth=function(t,n,r){if(n===0)return[0,0];n||(n=Infinity),r=r||0;var i,s;for(s=0;sn)break}return[r,s]}},this.destroy=function(){this.destroyed||(this.bgTokenizer.setDocument(null),this.bgTokenizer.cleanup(),this.destroyed=!0),this.$stopWorker(),this.removeAllListeners(),this.doc&&this.doc.off("change",this.$onChange),this.selection.detach()},this.isFullWidth=v}.call(d.prototype),e("./edit_session/folding").Folding.call(d.prototype),e("./edit_session/bracket_match").BracketMatch.call(d.prototype),o.defineOptions(d.prototype,"session",{wrap:{set:function(e){!e||e=="off"?e=!1:e=="free"?e=!0:e=="printMargin"?e=-1:typeof e=="string"&&(e=parseInt(e,10)||!1);if(this.$wrap==e)return;this.$wrap=e;if(!e)this.setUseWrapMode(!1);else{var t=typeof e=="number"?e:null;this.setWrapLimitRange(t,t),this.setUseWrapMode(!0)}},get:function(){return this.getUseWrapMode()?this.$wrap==-1?"printMargin":this.getWrapLimitRange().min?this.$wrap:"free":"off"},handlesSet:!0},wrapMethod:{set:function(e){e=e=="auto"?this.$mode.type!="text":e!="text",e!=this.$wrapAsCode&&(this.$wrapAsCode=e,this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0)))},initialValue:"auto"},indentedSoftWrap:{set:function(){this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0))},initialValue:!0},firstLineNumber:{set:function(){this._signal("changeBreakpoint")},initialValue:1},useWorker:{set:function(e){this.$useWorker=e,this.$stopWorker(),e&&this.$startWorker()},initialValue:!0},useSoftTabs:{initialValue:!0},tabSize:{set:function(e){e=parseInt(e),e>0&&this.$tabSize!==e&&(this.$modified=!0,this.$rowLengthCache=[],this.$tabSize=e,this._signal("changeTabSize"))},initialValue:4,handlesSet:!0},navigateWithinSoftTabs:{initialValue:!1},foldStyle:{set:function(e){this.setFoldStyle(e)},handlesSet:!0},overwrite:{set:function(e){this._signal("changeOverwrite")},initialValue:!1},newLineMode:{set:function(e){this.doc.setNewLineMode(e)},get:function(){return this.doc.getNewLineMode()},handlesSet:!0},mode:{set:function(e){this.setMode(e)},get:function(){return this.$modeId},handlesSet:!0}}),t.EditSession=d}),ace.define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";function u(e,t){function n(e){return/\w/.test(e)||t.regExp?"\\b":""}return n(e[0])+e+n(e[e.length-1])}var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(){this.$options={}};(function(){this.set=function(e){return i.mixin(this.$options,e),this},this.getOptions=function(){return r.copyObject(this.$options)},this.setOptions=function(e){this.$options=e},this.find=function(e){var t=this.$options,n=this.$matchIterator(e,t);if(!n)return!1;var r=null;return n.forEach(function(e,n,i,o){return r=new s(e,n,i,o),n==o&&t.start&&t.start.start&&t.skipCurrent!=0&&r.isEqual(t.start)?(r=null,!1):!0}),r},this.findAll=function(e){var t=this.$options;if(!t.needle)return[];this.$assembleRegExp(t);var n=t.range,i=n?e.getLines(n.start.row,n.end.row):e.doc.getAllLines(),o=[],u=t.re;if(t.$isMultiLine){var a=u.length,f=i.length-a,l;e:for(var c=u.offset||0;c<=f;c++){for(var h=0;hv)continue;o.push(l=new s(c,v,c+a-1,m)),a>2&&(c=c+a-2)}}else for(var g=0;gE&&o[h].end.row==S)h--;o=o.slice(g,h+1);for(g=0,h=o.length;g=u;n--)if(c(n,Number.MAX_VALUE,e))return;if(t.wrap==0)return;for(n=a,u=o.row;n>=u;n--)if(c(n,Number.MAX_VALUE,e))return};else var f=function(e){var n=o.row;if(c(n,o.column,e))return;for(n+=1;n<=a;n++)if(c(n,0,e))return;if(t.wrap==0)return;for(n=u,a=o.row;n<=a;n++)if(c(n,0,e))return};if(t.$isMultiLine)var l=n.length,c=function(t,i,s){var o=r?t-l+1:t;if(o<0||o+l>e.getLength())return;var u=e.getLine(o),a=u.search(n[0]);if(!r&&ai)return;if(s(o,a,o+l-1,c))return!0};else if(r)var c=function(t,r,i){var s=e.getLine(t),o=[],u,a=0;n.lastIndex=0;while(u=n.exec(s)){var f=u[0].length;a=u.index;if(!f){if(a>=s.length)break;n.lastIndex=a+=1}if(u.index+f>r)break;o.push(u.index,f)}for(var l=o.length-1;l>=0;l-=2){var c=o[l-1],f=o[l];if(i(t,c,t,c+f))return!0}};else var c=function(t,r,i){var s=e.getLine(t),o,u;n.lastIndex=r;while(u=n.exec(s)){var a=u[0].length;o=u.index;if(i(t,o,t,o+a))return!0;if(!a){n.lastIndex=o+=1;if(o>=s.length)return!1}}};return{forEach:f}}}).call(o.prototype),t.Search=o}),ace.define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function o(e,t){this.platform=t||(i.isMac?"mac":"win"),this.commands={},this.commandKeyBinding={},this.addCommands(e),this.$singleCommand=!0}function u(e,t){o.call(this,e,t),this.$singleCommand=!1}var r=e("../lib/keys"),i=e("../lib/useragent"),s=r.KEY_MODS;u.prototype=o.prototype,function(){function e(e){return typeof e=="object"&&e.bindKey&&e.bindKey.position||(e.isDefault?-100:0)}this.addCommand=function(e){this.commands[e.name]&&this.removeCommand(e),this.commands[e.name]=e,e.bindKey&&this._buildKeyHash(e)},this.removeCommand=function(e,t){var n=e&&(typeof e=="string"?e:e.name);e=this.commands[n],t||delete this.commands[n];var r=this.commandKeyBinding;for(var i in r){var s=r[i];if(s==e)delete r[i];else if(Array.isArray(s)){var o=s.indexOf(e);o!=-1&&(s.splice(o,1),s.length==1&&(r[i]=s[0]))}}},this.bindKey=function(e,t,n){typeof e=="object"&&e&&(n==undefined&&(n=e.position),e=e[this.platform]);if(!e)return;if(typeof t=="function")return this.addCommand({exec:t,bindKey:e,name:t.name||e});e.split("|").forEach(function(e){var r="";if(e.indexOf(" ")!=-1){var i=e.split(/\s+/);e=i.pop(),i.forEach(function(e){var t=this.parseKeys(e),n=s[t.hashId]+t.key;r+=(r?" ":"")+n,this._addCommandToBinding(r,"chainKeys")},this),r+=" "}var o=this.parseKeys(e),u=s[o.hashId]+o.key;this._addCommandToBinding(r+u,t,n)},this)},this._addCommandToBinding=function(t,n,r){var i=this.commandKeyBinding,s;if(!n)delete i[t];else if(!i[t]||this.$singleCommand)i[t]=n;else{Array.isArray(i[t])?(s=i[t].indexOf(n))!=-1&&i[t].splice(s,1):i[t]=[i[t]],typeof r!="number"&&(r=e(n));var o=i[t];for(s=0;sr)break}o.splice(s,0,n)}},this.addCommands=function(e){e&&Object.keys(e).forEach(function(t){var n=e[t];if(!n)return;if(typeof n=="string")return this.bindKey(n,t);typeof n=="function"&&(n={exec:n});if(typeof n!="object")return;n.name||(n.name=t),this.addCommand(n)},this)},this.removeCommands=function(e){Object.keys(e).forEach(function(t){this.removeCommand(e[t])},this)},this.bindKeys=function(e){Object.keys(e).forEach(function(t){this.bindKey(t,e[t])},this)},this._buildKeyHash=function(e){this.bindKey(e.bindKey,e)},this.parseKeys=function(e){var t=e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(e){return e}),n=t.pop(),i=r[n];if(r.FUNCTION_KEYS[i])n=r.FUNCTION_KEYS[i].toLowerCase();else{if(!t.length)return{key:n,hashId:-1};if(t.length==1&&t[0]=="shift")return{key:n.toUpperCase(),hashId:-1}}var s=0;for(var o=t.length;o--;){var u=r.KEY_MODS[t[o]];if(u==null)return typeof console!="undefined"&&console.error("invalid modifier "+t[o]+" in "+e),!1;s|=u}return{key:n,hashId:s}},this.findKeyCommand=function(t,n){var r=s[t]+n;return this.commandKeyBinding[r]},this.handleKeyboard=function(e,t,n,r){if(r<0)return;var i=s[t]+n,o=this.commandKeyBinding[i];e.$keyChain&&(e.$keyChain+=" "+i,o=this.commandKeyBinding[e.$keyChain]||o);if(o)if(o=="chainKeys"||o[o.length-1]=="chainKeys")return e.$keyChain=e.$keyChain||i,{command:"null"};if(e.$keyChain)if(!!t&&t!=4||n.length!=1){if(t==-1||r>0)e.$keyChain=""}else e.$keyChain=e.$keyChain.slice(0,-i.length-1);return{command:o}},this.getStatusText=function(e,t){return t.$keyChain||""}}.call(o.prototype),t.HashHandler=o,t.MultiHashHandler=u}),ace.define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.args?e.command.exec(e.editor,e.args,e.event,!1):e.command.exec(e.editor,{},e.event,!0)})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;if(this.$checkCommandState!=0&&e.isAvailable&&!e.isAvailable(t))return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.off("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}),ace.define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"],function(e,t,n){"use strict";function o(e,t){return{win:e,mac:t}}var r=e("../lib/lang"),i=e("../config"),s=e("../range").Range;t.commands=[{name:"showSettingsMenu",description:"Show settings menu",bindKey:o("Ctrl-,","Command-,"),exec:function(e){i.loadModule("ace/ext/settings_menu",function(t){t.init(e),e.showSettingsMenu()})},readOnly:!0},{name:"goToNextError",description:"Go to next error",bindKey:o("Alt-E","F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,1)})},scrollIntoView:"animate",readOnly:!0},{name:"goToPreviousError",description:"Go to previous error",bindKey:o("Alt-Shift-E","Shift-F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,-1)})},scrollIntoView:"animate",readOnly:!0},{name:"selectall",description:"Select all",bindKey:o("Ctrl-A","Command-A"),exec:function(e){e.selectAll()},readOnly:!0},{name:"centerselection",description:"Center selection",bindKey:o(null,"Ctrl-L"),exec:function(e){e.centerSelection()},readOnly:!0},{name:"gotoline",description:"Go to line...",bindKey:o("Ctrl-L","Command-L"),exec:function(e,t){typeof t=="number"&&!isNaN(t)&&e.gotoLine(t),e.prompt({$type:"gotoLine"})},readOnly:!0},{name:"fold",bindKey:o("Alt-L|Ctrl-F1","Command-Alt-L|Command-F1"),exec:function(e){e.session.toggleFold(!1)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"unfold",bindKey:o("Alt-Shift-L|Ctrl-Shift-F1","Command-Alt-Shift-L|Command-Shift-F1"),exec:function(e){e.session.toggleFold(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleFoldWidget",description:"Toggle fold widget",bindKey:o("F2","F2"),exec:function(e){e.session.toggleFoldWidget()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleParentFoldWidget",description:"Toggle parent fold widget",bindKey:o("Alt-F2","Alt-F2"),exec:function(e){e.session.toggleFoldWidget(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"foldall",description:"Fold all",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAll()},scrollIntoView:"center",readOnly:!0},{name:"foldAllComments",description:"Fold all comments",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAllComments()},scrollIntoView:"center",readOnly:!0},{name:"foldOther",description:"Fold other",bindKey:o("Alt-0","Command-Option-0"),exec:function(e){e.session.foldAll(),e.session.unfold(e.selection.getAllRanges())},scrollIntoView:"center",readOnly:!0},{name:"unfoldall",description:"Unfold all",bindKey:o("Alt-Shift-0","Command-Option-Shift-0"),exec:function(e){e.session.unfold()},scrollIntoView:"center",readOnly:!0},{name:"findnext",description:"Find next",bindKey:o("Ctrl-K","Command-G"),exec:function(e){e.findNext()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"findprevious",description:"Find previous",bindKey:o("Ctrl-Shift-K","Command-Shift-G"),exec:function(e){e.findPrevious()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"selectOrFindNext",description:"Select or find next",bindKey:o("Alt-K","Ctrl-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findNext()},readOnly:!0},{name:"selectOrFindPrevious",description:"Select or find previous",bindKey:o("Alt-Shift-K","Ctrl-Shift-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findPrevious()},readOnly:!0},{name:"find",description:"Find",bindKey:o("Ctrl-F","Command-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e)})},readOnly:!0},{name:"overwrite",description:"Overwrite",bindKey:"Insert",exec:function(e){e.toggleOverwrite()},readOnly:!0},{name:"selecttostart",description:"Select to start",bindKey:o("Ctrl-Shift-Home","Command-Shift-Home|Command-Shift-Up"),exec:function(e){e.getSelection().selectFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotostart",description:"Go to start",bindKey:o("Ctrl-Home","Command-Home|Command-Up"),exec:function(e){e.navigateFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectup",description:"Select up",bindKey:o("Shift-Up","Shift-Up|Ctrl-Shift-P"),exec:function(e){e.getSelection().selectUp()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golineup",description:"Go line up",bindKey:o("Up","Up|Ctrl-P"),exec:function(e,t){e.navigateUp(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttoend",description:"Select to end",bindKey:o("Ctrl-Shift-End","Command-Shift-End|Command-Shift-Down"),exec:function(e){e.getSelection().selectFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotoend",description:"Go to end",bindKey:o("Ctrl-End","Command-End|Command-Down"),exec:function(e){e.navigateFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectdown",description:"Select down",bindKey:o("Shift-Down","Shift-Down|Ctrl-Shift-N"),exec:function(e){e.getSelection().selectDown()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golinedown",description:"Go line down",bindKey:o("Down","Down|Ctrl-N"),exec:function(e,t){e.navigateDown(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordleft",description:"Select word left",bindKey:o("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(e){e.getSelection().selectWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordleft",description:"Go to word left",bindKey:o("Ctrl-Left","Option-Left"),exec:function(e){e.navigateWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolinestart",description:"Select to line start",bindKey:o("Alt-Shift-Left","Command-Shift-Left|Ctrl-Shift-A"),exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolinestart",description:"Go to line start",bindKey:o("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(e){e.navigateLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectleft",description:"Select left",bindKey:o("Shift-Left","Shift-Left|Ctrl-Shift-B"),exec:function(e){e.getSelection().selectLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoleft",description:"Go to left",bindKey:o("Left","Left|Ctrl-B"),exec:function(e,t){e.navigateLeft(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordright",description:"Select word right",bindKey:o("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(e){e.getSelection().selectWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordright",description:"Go to word right",bindKey:o("Ctrl-Right","Option-Right"),exec:function(e){e.navigateWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolineend",description:"Select to line end",bindKey:o("Alt-Shift-Right","Command-Shift-Right|Shift-End|Ctrl-Shift-E"),exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolineend",description:"Go to line end",bindKey:o("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(e){e.navigateLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectright",description:"Select right",bindKey:o("Shift-Right","Shift-Right"),exec:function(e){e.getSelection().selectRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoright",description:"Go to right",bindKey:o("Right","Right|Ctrl-F"),exec:function(e,t){e.navigateRight(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectpagedown",description:"Select page down",bindKey:"Shift-PageDown",exec:function(e){e.selectPageDown()},readOnly:!0},{name:"pagedown",description:"Page down",bindKey:o(null,"Option-PageDown"),exec:function(e){e.scrollPageDown()},readOnly:!0},{name:"gotopagedown",description:"Go to page down",bindKey:o("PageDown","PageDown|Ctrl-V"),exec:function(e){e.gotoPageDown()},readOnly:!0},{name:"selectpageup",description:"Select page up",bindKey:"Shift-PageUp",exec:function(e){e.selectPageUp()},readOnly:!0},{name:"pageup",description:"Page up",bindKey:o(null,"Option-PageUp"),exec:function(e){e.scrollPageUp()},readOnly:!0},{name:"gotopageup",description:"Go to page up",bindKey:"PageUp",exec:function(e){e.gotoPageUp()},readOnly:!0},{name:"scrollup",description:"Scroll up",bindKey:o("Ctrl-Up",null),exec:function(e){e.renderer.scrollBy(0,-2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"scrolldown",description:"Scroll down",bindKey:o("Ctrl-Down",null),exec:function(e){e.renderer.scrollBy(0,2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"selectlinestart",description:"Select line start",bindKey:"Shift-Home",exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectlineend",description:"Select line end",bindKey:"Shift-End",exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"togglerecording",description:"Toggle recording",bindKey:o("Ctrl-Alt-E","Command-Option-E"),exec:function(e){e.commands.toggleRecording(e)},readOnly:!0},{name:"replaymacro",description:"Replay macro",bindKey:o("Ctrl-Shift-E","Command-Shift-E"),exec:function(e){e.commands.replay(e)},readOnly:!0},{name:"jumptomatching",description:"Jump to matching",bindKey:o("Ctrl-\\|Ctrl-P","Command-\\"),exec:function(e){e.jumpToMatching()},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"selecttomatching",description:"Select to matching",bindKey:o("Ctrl-Shift-\\|Ctrl-Shift-P","Command-Shift-\\"),exec:function(e){e.jumpToMatching(!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"expandToMatching",description:"Expand to matching",bindKey:o("Ctrl-Shift-M","Ctrl-Shift-M"),exec:function(e){e.jumpToMatching(!0,!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"passKeysToBrowser",description:"Pass keys to browser",bindKey:o(null,null),exec:function(){},passEvent:!0,readOnly:!0},{name:"copy",description:"Copy",exec:function(e){},readOnly:!0},{name:"cut",description:"Cut",exec:function(e){var t=e.$copyWithEmptySelection&&e.selection.isEmpty(),n=t?e.selection.getLineRange():e.selection.getRange();e._emit("cut",n),n.isEmpty()||e.session.remove(n),e.clearSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"paste",description:"Paste",exec:function(e,t){e.$handlePaste(t)},scrollIntoView:"cursor"},{name:"removeline",description:"Remove line",bindKey:o("Ctrl-D","Command-D"),exec:function(e){e.removeLines()},scrollIntoView:"cursor",multiSelectAction:"forEachLine"},{name:"duplicateSelection",description:"Duplicate selection",bindKey:o("Ctrl-Shift-D","Command-Shift-D"),exec:function(e){e.duplicateSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"sortlines",description:"Sort lines",bindKey:o("Ctrl-Alt-S","Command-Alt-S"),exec:function(e){e.sortLines()},scrollIntoView:"selection",multiSelectAction:"forEachLine"},{name:"togglecomment",description:"Toggle comment",bindKey:o("Ctrl-/","Command-/"),exec:function(e){e.toggleCommentLines()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"toggleBlockComment",description:"Toggle block comment",bindKey:o("Ctrl-Shift-/","Command-Shift-/"),exec:function(e){e.toggleBlockComment()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"modifyNumberUp",description:"Modify number up",bindKey:o("Ctrl-Shift-Up","Alt-Shift-Up"),exec:function(e){e.modifyNumber(1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"modifyNumberDown",description:"Modify number down",bindKey:o("Ctrl-Shift-Down","Alt-Shift-Down"),exec:function(e){e.modifyNumber(-1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"replace",description:"Replace",bindKey:o("Ctrl-H","Command-Option-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e,!0)})}},{name:"undo",description:"Undo",bindKey:o("Ctrl-Z","Command-Z"),exec:function(e){e.undo()}},{name:"redo",description:"Redo",bindKey:o("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(e){e.redo()}},{name:"copylinesup",description:"Copy lines up",bindKey:o("Alt-Shift-Up","Command-Option-Up"),exec:function(e){e.copyLinesUp()},scrollIntoView:"cursor"},{name:"movelinesup",description:"Move lines up",bindKey:o("Alt-Up","Option-Up"),exec:function(e){e.moveLinesUp()},scrollIntoView:"cursor"},{name:"copylinesdown",description:"Copy lines down",bindKey:o("Alt-Shift-Down","Command-Option-Down"),exec:function(e){e.copyLinesDown()},scrollIntoView:"cursor"},{name:"movelinesdown",description:"Move lines down",bindKey:o("Alt-Down","Option-Down"),exec:function(e){e.moveLinesDown()},scrollIntoView:"cursor"},{name:"del",description:"Delete",bindKey:o("Delete","Delete|Ctrl-D|Shift-Delete"),exec:function(e){e.remove("right")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"backspace",description:"Backspace",bindKey:o("Shift-Backspace|Backspace","Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(e){e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"cut_or_delete",description:"Cut or delete",bindKey:o("Shift-Delete",null),exec:function(e){if(!e.selection.isEmpty())return!1;e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestart",description:"Remove to line start",bindKey:o("Alt-Backspace","Command-Backspace"),exec:function(e){e.removeToLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineend",description:"Remove to line end",bindKey:o("Alt-Delete","Ctrl-K|Command-Delete"),exec:function(e){e.removeToLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestarthard",description:"Remove to line start hard",bindKey:o("Ctrl-Shift-Backspace",null),exec:function(e){var t=e.selection.getRange();t.start.column=0,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineendhard",description:"Remove to line end hard",bindKey:o("Ctrl-Shift-Delete",null),exec:function(e){var t=e.selection.getRange();t.end.column=Number.MAX_VALUE,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordleft",description:"Remove word left",bindKey:o("Ctrl-Backspace","Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(e){e.removeWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordright",description:"Remove word right",bindKey:o("Ctrl-Delete","Alt-Delete"),exec:function(e){e.removeWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"outdent",description:"Outdent",bindKey:o("Shift-Tab","Shift-Tab"),exec:function(e){e.blockOutdent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"indent",description:"Indent",bindKey:o("Tab","Tab"),exec:function(e){e.indent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"blockoutdent",description:"Block outdent",bindKey:o("Ctrl-[","Ctrl-["),exec:function(e){e.blockOutdent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"blockindent",description:"Block indent",bindKey:o("Ctrl-]","Ctrl-]"),exec:function(e){e.blockIndent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"insertstring",description:"Insert string",exec:function(e,t){e.insert(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"inserttext",description:"Insert text",exec:function(e,t){e.insert(r.stringRepeat(t.text||"",t.times||1))},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"splitline",description:"Split line",bindKey:o(null,"Ctrl-O"),exec:function(e){e.splitLine()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"transposeletters",description:"Transpose letters",bindKey:o("Alt-Shift-X","Ctrl-T"),exec:function(e){e.transposeLetters()},multiSelectAction:function(e){e.transposeSelections(1)},scrollIntoView:"cursor"},{name:"touppercase",description:"To uppercase",bindKey:o("Ctrl-U","Ctrl-U"),exec:function(e){e.toUpperCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"tolowercase",description:"To lowercase",bindKey:o("Ctrl-Shift-U","Ctrl-Shift-U"),exec:function(e){e.toLowerCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"autoindent",description:"Auto Indent",bindKey:o(null,null),exec:function(e){e.autoIndent()},multiSelectAction:"forEachLine",scrollIntoView:"animate"},{name:"expandtoline",description:"Expand to line",bindKey:o("Ctrl-Shift-L","Command-Shift-L"),exec:function(e){var t=e.selection.getRange();t.start.column=t.end.column=0,t.end.row++,e.selection.setRange(t,!1)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"openlink",bindKey:o("Ctrl+F3","F3"),exec:function(e){e.openLink()}},{name:"joinlines",description:"Join lines",bindKey:o(null,null),exec:function(e){var t=e.selection.isBackwards(),n=t?e.selection.getSelectionLead():e.selection.getSelectionAnchor(),i=t?e.selection.getSelectionAnchor():e.selection.getSelectionLead(),o=e.session.doc.getLine(n.row).length,u=e.session.doc.getTextRange(e.selection.getRange()),a=u.replace(/\n\s*/," ").length,f=e.session.doc.getLine(n.row);for(var l=n.row+1;l<=i.row+1;l++){var c=r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l)));c.length!==0&&(c=" "+c),f+=c}i.row+10?(e.selection.moveCursorTo(n.row,n.column),e.selection.selectTo(n.row,n.column+a)):(o=e.session.doc.getLine(n.row).length>o?o+1:o,e.selection.moveCursorTo(n.row,o))},multiSelectAction:"forEach",readOnly:!0},{name:"invertSelection",description:"Invert selection",bindKey:o(null,null),exec:function(e){var t=e.session.doc.getLength()-1,n=e.session.doc.getLine(t).length,r=e.selection.rangeList.ranges,i=[];r.length<1&&(r=[e.selection.getRange()]);for(var o=0;ot[n].column&&n++,s.unshift(n,0),t.splice.apply(t,s),this.$updateRows()}},this.$updateRows=function(){var e=this.session.lineWidgets;if(!e)return;var t=!0;e.forEach(function(e,n){if(e){t=!1,e.row=n;while(e.$oldWidget)e.$oldWidget.row=n,e=e.$oldWidget}}),t&&(this.session.lineWidgets=null)},this.$registerLineWidget=function(e){this.session.lineWidgets||(this.session.lineWidgets=new Array(this.session.getLength()));var t=this.session.lineWidgets[e.row];return t&&(e.$oldWidget=t,t.el&&t.el.parentNode&&(t.el.parentNode.removeChild(t.el),t._inDocument=!1)),this.session.lineWidgets[e.row]=e,e},this.addLineWidget=function(e){this.$registerLineWidget(e),e.session=this.session;if(!this.editor)return e;var t=this.editor.renderer;e.html&&!e.el&&(e.el=r.createElement("div"),e.el.innerHTML=e.html),e.text&&!e.el&&(e.el=r.createElement("div"),e.el.textContent=e.text),e.el&&(r.addCssClass(e.el,"ace_lineWidgetContainer"),e.className&&r.addCssClass(e.el,e.className),e.el.style.position="absolute",e.el.style.zIndex=5,t.container.appendChild(e.el),e._inDocument=!0,e.coverGutter||(e.el.style.zIndex=3),e.pixelHeight==null&&(e.pixelHeight=e.el.offsetHeight)),e.rowCount==null&&(e.rowCount=e.pixelHeight/t.layerConfig.lineHeight);var n=this.session.getFoldAt(e.row,0);e.$fold=n;if(n){var i=this.session.lineWidgets;e.row==n.end.row&&!i[n.start.row]?i[n.start.row]=e:e.hidden=!0}return this.session._emit("changeFold",{data:{start:{row:e.row}}}),this.$updateRows(),this.renderWidgets(null,t),this.onWidgetChanged(e),e},this.removeLineWidget=function(e){e._inDocument=!1,e.session=null,e.el&&e.el.parentNode&&e.el.parentNode.removeChild(e.el);if(e.editor&&e.editor.destroy)try{e.editor.destroy()}catch(t){}if(this.session.lineWidgets){var n=this.session.lineWidgets[e.row];if(n==e)this.session.lineWidgets[e.row]=e.$oldWidget,e.$oldWidget&&this.onWidgetChanged(e.$oldWidget);else while(n){if(n.$oldWidget==e){n.$oldWidget=e.$oldWidget;break}n=n.$oldWidget}}this.session._emit("changeFold",{data:{start:{row:e.row}}}),this.$updateRows()},this.getWidgetsAtRow=function(e){var t=this.session.lineWidgets,n=t&&t[e],r=[];while(n)r.push(n),n=n.$oldWidget;return r},this.onWidgetChanged=function(e){this.session._changedWidgets.push(e),this.editor&&this.editor.renderer.updateFull()},this.measureWidgets=function(e,t){var n=this.session._changedWidgets,r=t.layerConfig;if(!n||!n.length)return;var i=Infinity;for(var s=0;s0&&!r[i])i--;this.firstRow=n.firstRow,this.lastRow=n.lastRow,t.$cursorLayer.config=n;for(var o=i;o<=s;o++){var u=r[o];if(!u||!u.el)continue;if(u.hidden){u.el.style.top=-100-(u.pixelHeight||0)+"px";continue}u._inDocument||(u._inDocument=!0,t.container.appendChild(u.el));var a=t.$cursorLayer.getPixelPosition({row:o,column:0},!0).top;u.coverLine||(a+=n.lineHeight*this.session.getRowLineCount(u.row)),u.el.style.top=a-n.offset+"px";var f=u.coverGutter?0:t.gutterWidth;u.fixedWidth||(f-=t.scrollLeft),u.el.style.left=f+"px",u.fullWidth&&u.screenWidth&&(u.el.style.minWidth=n.width+2*n.padding+"px"),u.fixedWidth?u.el.style.right=t.scrollBar.getWidth()+"px":u.el.style.right=""}}}).call(i.prototype),t.LineWidgets=i}),ace.define("ace/editor",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/keyboard/textinput","ace/mouse/mouse_handler","ace/mouse/fold_handler","ace/keyboard/keybinding","ace/edit_session","ace/search","ace/range","ace/lib/event_emitter","ace/commands/command_manager","ace/commands/default_commands","ace/config","ace/token_iterator","ace/line_widgets","ace/clipboard"],function(e,t,n){"use strict";var r=this&&this.__values||function(e){var t=typeof Symbol=="function"&&Symbol.iterator,n=t&&e[t],r=0;if(n)return n.call(e);if(e&&typeof e.length=="number")return{next:function(){return e&&r>=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")},i=e("./lib/oop"),s=e("./lib/dom"),o=e("./lib/lang"),u=e("./lib/useragent"),a=e("./keyboard/textinput").TextInput,f=e("./mouse/mouse_handler").MouseHandler,l=e("./mouse/fold_handler").FoldHandler,c=e("./keyboard/keybinding").KeyBinding,h=e("./edit_session").EditSession,p=e("./search").Search,d=e("./range").Range,v=e("./lib/event_emitter").EventEmitter,m=e("./commands/command_manager").CommandManager,g=e("./commands/default_commands").commands,y=e("./config"),b=e("./token_iterator").TokenIterator,w=e("./line_widgets").LineWidgets,E=e("./clipboard"),S=function(e,t,n){this.$toDestroy=[];var r=e.getContainerElement();this.container=r,this.renderer=e,this.id="editor"+ ++S.$uid,this.commands=new m(u.isMac?"mac":"win",g),typeof document=="object"&&(this.textInput=new a(e.getTextAreaContainer(),this),this.renderer.textarea=this.textInput.getElement(),this.$mouseHandler=new f(this),new l(this)),this.keyBinding=new c(this),this.$search=(new p).set({wrap:!0}),this.$historyTracker=this.$historyTracker.bind(this),this.commands.on("exec",this.$historyTracker),this.$initOperationListeners(),this._$emitInputEvent=o.delayedCall(function(){this._signal("input",{}),this.session&&!this.session.destroyed&&this.session.bgTokenizer.scheduleStart()}.bind(this)),this.on("change",function(e,t){t._$emitInputEvent.schedule(31)}),this.setSession(t||n&&n.session||new h("")),y.resetOptions(this),n&&this.setOptions(n),y._signal("editor",this)};S.$uid=0,function(){i.implement(this,v),this.$initOperationListeners=function(){this.commands.on("exec",this.startOperation.bind(this),!0),this.commands.on("afterExec",this.endOperation.bind(this),!0),this.$opResetTimer=o.delayedCall(this.endOperation.bind(this,!0)),this.on("change",function(){this.curOp||(this.startOperation(),this.curOp.selectionBefore=this.$lastSel),this.curOp.docChanged=!0}.bind(this),!0),this.on("changeSelection",function(){this.curOp||(this.startOperation(),this.curOp.selectionBefore=this.$lastSel),this.curOp.selectionChanged=!0}.bind(this),!0)},this.curOp=null,this.prevOp={},this.startOperation=function(e){if(this.curOp){if(!e||this.curOp.command)return;this.prevOp=this.curOp}e||(this.previousCommand=null,e={}),this.$opResetTimer.schedule(),this.curOp=this.session.curOp={command:e.command||{},args:e.args,scrollTop:this.renderer.scrollTop},this.curOp.selectionBefore=this.selection.toJSON()},this.endOperation=function(e){if(this.curOp&&this.session){if(e&&e.returnValue===!1||!this.session)return this.curOp=null;if(e==1&&this.curOp.command&&this.curOp.command.name=="mouse")return;this._signal("beforeEndOperation");if(!this.curOp)return;var t=this.curOp.command,n=t&&t.scrollIntoView;if(n){switch(n){case"center-animate":n="animate";case"center":this.renderer.scrollCursorIntoView(null,.5);break;case"animate":case"cursor":this.renderer.scrollCursorIntoView();break;case"selectionPart":var r=this.selection.getRange(),i=this.renderer.layerConfig;(r.start.row>=i.lastRow||r.end.row<=i.firstRow)&&this.renderer.scrollSelectionIntoView(this.selection.anchor,this.selection.lead);break;default:}n=="animate"&&this.renderer.animateScrolling(this.curOp.scrollTop)}var s=this.selection.toJSON();this.curOp.selectionAfter=s,this.$lastSel=this.selection.toJSON(),this.session.getUndoManager().addSelection(s),this.prevOp=this.curOp,this.curOp=null}},this.$mergeableCommands=["backspace","del","insertstring"],this.$historyTracker=function(e){if(!this.$mergeUndoDeltas)return;var t=this.prevOp,n=this.$mergeableCommands,r=t.command&&e.command.name==t.command.name;if(e.command.name=="insertstring"){var i=e.args;this.mergeNextCommand===undefined&&(this.mergeNextCommand=!0),r=r&&this.mergeNextCommand&&(!/\s/.test(i)||/\s/.test(t.args)),this.mergeNextCommand=!0}else r=r&&n.indexOf(e.command.name)!==-1;this.$mergeUndoDeltas!="always"&&Date.now()-this.sequenceStartTime>2e3&&(r=!1),r?this.session.mergeUndoDeltas=!0:n.indexOf(e.command.name)!==-1&&(this.sequenceStartTime=Date.now())},this.setKeyboardHandler=function(e,t){if(e&&typeof e=="string"&&e!="ace"){this.$keybindingId=e;var n=this;y.loadModule(["keybinding",e],function(r){n.$keybindingId==e&&n.keyBinding.setKeyboardHandler(r&&r.handler),t&&t()})}else this.$keybindingId=null,this.keyBinding.setKeyboardHandler(e),t&&t()},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(e){if(this.session==e)return;this.curOp&&this.endOperation(),this.curOp={};var t=this.session;if(t){this.session.off("change",this.$onDocumentChange),this.session.off("changeMode",this.$onChangeMode),this.session.off("tokenizerUpdate",this.$onTokenizerUpdate),this.session.off("changeTabSize",this.$onChangeTabSize),this.session.off("changeWrapLimit",this.$onChangeWrapLimit),this.session.off("changeWrapMode",this.$onChangeWrapMode),this.session.off("changeFold",this.$onChangeFold),this.session.off("changeFrontMarker",this.$onChangeFrontMarker),this.session.off("changeBackMarker",this.$onChangeBackMarker),this.session.off("changeBreakpoint",this.$onChangeBreakpoint),this.session.off("changeAnnotation",this.$onChangeAnnotation),this.session.off("changeOverwrite",this.$onCursorChange),this.session.off("changeScrollTop",this.$onScrollTopChange),this.session.off("changeScrollLeft",this.$onScrollLeftChange);var n=this.session.getSelection();n.off("changeCursor",this.$onCursorChange),n.off("changeSelection",this.$onSelectionChange)}this.session=e,e?(this.$onDocumentChange=this.onDocumentChange.bind(this),e.on("change",this.$onDocumentChange),this.renderer.setSession(e),this.$onChangeMode=this.onChangeMode.bind(this),e.on("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),e.on("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.onChangeTabSize.bind(this.renderer),e.on("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),e.on("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),e.on("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),e.on("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.on("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.on("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.on("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.on("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.on("changeOverwrite",this.$onCursorChange),this.$onScrollTopChange=this.onScrollTopChange.bind(this),this.session.on("changeScrollTop",this.$onScrollTopChange),this.$onScrollLeftChange=this.onScrollLeftChange.bind(this),this.session.on("changeScrollLeft",this.$onScrollLeftChange),this.selection=e.getSelection(),this.selection.on("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.on("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.onCursorChange(),this.onScrollTopChange(),this.onScrollLeftChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.session.getUseWrapMode()&&this.renderer.adjustWrapLimit(),this.renderer.updateFull()):(this.selection=null,this.renderer.setSession(e)),this._signal("changeSession",{session:e,oldSession:t}),this.curOp=null,t&&t._signal("changeEditor",{oldEditor:this}),e&&e._signal("changeEditor",{editor:this}),e&&!e.destroyed&&e.bgTokenizer.scheduleStart()},this.getSession=function(){return this.session},this.setValue=function(e,t){return this.session.doc.setValue(e),t?t==1?this.navigateFileEnd():t==-1&&this.navigateFileStart():this.selectAll(),e},this.getValue=function(){return this.session.getValue()},this.getSelection=function(){return this.selection},this.resize=function(e){this.renderer.onResize(e)},this.setTheme=function(e,t){this.renderer.setTheme(e,t)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(e){this.renderer.setStyle(e)},this.unsetStyle=function(e){this.renderer.unsetStyle(e)},this.getFontSize=function(){return this.getOption("fontSize")||s.computedStyle(this.container).fontSize},this.setFontSize=function(e){this.setOption("fontSize",e)},this.$highlightBrackets=function(){if(this.$highlightPending)return;var e=this;this.$highlightPending=!0,setTimeout(function(){e.$highlightPending=!1;var t=e.session;if(!t||t.destroyed)return;t.$bracketHighlight&&(t.$bracketHighlight.markerIds.forEach(function(e){t.removeMarker(e)}),t.$bracketHighlight=null);var n=e.getCursorPosition(),r=e.getKeyboardHandler(),i=r&&r.$getDirectionForHighlight&&r.$getDirectionForHighlight(e),s=t.getMatchingBracketRanges(n,i);if(!s){var o=new b(t,n.row,n.column),u=o.getCurrentToken();if(u&&/\b(?:tag-open|tag-name)/.test(u.type)){var a=t.getMatchingTags(n);a&&(s=[a.openTagName,a.closeTagName])}}!s&&t.$mode.getMatching&&(s=t.$mode.getMatching(e.session));if(!s){e.getHighlightIndentGuides()&&e.renderer.$textLayer.$highlightIndentGuide();return}var f="ace_bracket";Array.isArray(s)?s.length==1&&(f="ace_error_bracket"):s=[s],s.length==2&&(d.comparePoints(s[0].end,s[1].start)==0?s=[d.fromPoints(s[0].start,s[1].end)]:d.comparePoints(s[0].start,s[1].end)==0&&(s=[d.fromPoints(s[1].start,s[0].end)])),t.$bracketHighlight={ranges:s,markerIds:s.map(function(e){return t.addMarker(e,f,"text")})},e.getHighlightIndentGuides()&&e.renderer.$textLayer.$highlightIndentGuide()},50)},this.focus=function(){this.textInput.focus()},this.isFocused=function(){return this.textInput.isFocused()},this.blur=function(){this.textInput.blur()},this.onFocus=function(e){if(this.$isFocused)return;this.$isFocused=!0,this.renderer.showCursor(),this.renderer.visualizeFocus(),this._emit("focus",e)},this.onBlur=function(e){if(!this.$isFocused)return;this.$isFocused=!1,this.renderer.hideCursor(),this.renderer.visualizeBlur(),this._emit("blur",e)},this.$cursorChange=function(){this.renderer.updateCursor(),this.$highlightBrackets(),this.$updateHighlightActiveLine()},this.onDocumentChange=function(e){var t=this.session.$useWrapMode,n=e.start.row==e.end.row?e.end.row:Infinity;this.renderer.updateLines(e.start.row,n,t),this._signal("change",e),this.$cursorChange()},this.onTokenizerUpdate=function(e){var t=e.data;this.renderer.updateLines(t.first,t.last)},this.onScrollTopChange=function(){this.renderer.scrollToY(this.session.getScrollTop())},this.onScrollLeftChange=function(){this.renderer.scrollToX(this.session.getScrollLeft())},this.onCursorChange=function(){this.$cursorChange(),this._signal("changeSelection")},this.$updateHighlightActiveLine=function(){var e=this.getSession(),t;if(this.$highlightActiveLine){if(this.$selectionStyle!="line"||!this.selection.isMultiLine())t=this.getCursorPosition();this.renderer.theme&&this.renderer.theme.$selectionColorConflict&&!this.selection.isEmpty()&&(t=!1),this.renderer.$maxLines&&this.session.getLength()===1&&!(this.renderer.$minLines>1)&&(t=!1)}if(e.$highlightLineMarker&&!t)e.removeMarker(e.$highlightLineMarker.id),e.$highlightLineMarker=null;else if(!e.$highlightLineMarker&&t){var n=new d(t.row,t.column,t.row,Infinity);n.id=e.addMarker(n,"ace_active-line","screenLine"),e.$highlightLineMarker=n}else t&&(e.$highlightLineMarker.start.row=t.row,e.$highlightLineMarker.end.row=t.row,e.$highlightLineMarker.start.column=t.column,e._signal("changeBackMarker"))},this.onSelectionChange=function(e){var t=this.session;t.$selectionMarker&&t.removeMarker(t.$selectionMarker),t.$selectionMarker=null;if(!this.selection.isEmpty()){var n=this.selection.getRange(),r=this.getSelectionStyle();t.$selectionMarker=t.addMarker(n,"ace_selection",r)}else this.$updateHighlightActiveLine();var i=this.$highlightSelectedWord&&this.$getSelectionHighLightRegexp();this.session.highlight(i),this._signal("changeSelection")},this.$getSelectionHighLightRegexp=function(){var e=this.session,t=this.getSelectionRange();if(t.isEmpty()||t.isMultiLine())return;var n=t.start.column,r=t.end.column,i=e.getLine(t.start.row),s=i.substring(n,r);if(s.length>5e3||!/[\w\d]/.test(s))return;var o=this.$search.$assembleRegExp({wholeWord:!0,caseSensitive:!0,needle:s}),u=i.substring(n-1,r+1);if(!o.test(u))return;return o},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.updateBreakpoints()},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(e){this.renderer.updateText(),this._emit("changeMode",e)},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getSelectedText=function(){return this.session.getTextRange(this.getSelectionRange())},this.getCopyText=function(){var e=this.getSelectedText(),t=this.session.doc.getNewLineCharacter(),n=!1;if(!e&&this.$copyWithEmptySelection){n=!0;var r=this.selection.getAllRanges();for(var i=0;iu.search(/\S|$/)){var a=u.substr(i.column).search(/\S|$/);n.doc.removeInLine(i.row,i.column,i.column+a)}}this.clearSelection();var f=i.column,l=n.getState(i.row),u=n.getLine(i.row),c=r.checkOutdent(l,u,e);n.insert(i,e),s&&s.selection&&(s.selection.length==2?this.selection.setSelectionRange(new d(i.row,f+s.selection[0],i.row,f+s.selection[1])):this.selection.setSelectionRange(new d(i.row+s.selection[0],s.selection[1],i.row+s.selection[2],s.selection[3])));if(this.$enableAutoIndent){if(n.getDocument().isNewLine(e)){var h=r.getNextLineIndent(l,u.slice(0,i.column),n.getTabString());n.insert({row:i.row+1,column:0},h)}c&&r.autoOutdent(l,n,i.row)}},this.autoIndent=function(){var e=this.session,t=e.getMode(),n,r;if(this.selection.isEmpty())n=0,r=e.doc.getLength()-1;else{var i=this.getSelectionRange();n=i.start.row,r=i.end.row}var s="",o="",u="",a,f,l,c=e.getTabString();for(var h=n;h<=r;h++)h>0&&(s=e.getState(h-1),o=e.getLine(h-1),u=t.getNextLineIndent(s,o,c)),a=e.getLine(h),f=t.$getIndent(a),u!==f&&(f.length>0&&(l=new d(h,0,h,f.length),e.remove(l)),u.length>0&&e.insert({row:h,column:0},u)),t.autoOutdent(s,e,h)},this.onTextInput=function(e,t){if(!t)return this.keyBinding.onTextInput(e);this.startOperation({command:{name:"insertstring"}});var n=this.applyComposition.bind(this,e,t);this.selection.rangeCount?this.forEachSelection(n):n(),this.endOperation()},this.applyComposition=function(e,t){if(t.extendLeft||t.extendRight){var n=this.selection.getRange();n.start.column-=t.extendLeft,n.end.column+=t.extendRight,n.start.column<0&&(n.start.row--,n.start.column+=this.session.getLine(n.start.row).length+1),this.selection.setRange(n),!e&&!n.isEmpty()&&this.remove()}(e||!this.selection.isEmpty())&&this.insert(e,!0);if(t.restoreStart||t.restoreEnd){var n=this.selection.getRange();n.start.column-=t.restoreStart,n.end.column-=t.restoreEnd,this.selection.setRange(n)}},this.onCommandKey=function(e,t,n){return this.keyBinding.onCommandKey(e,t,n)},this.setOverwrite=function(e){this.session.setOverwrite(e)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(e){this.setOption("scrollSpeed",e)},this.getScrollSpeed=function(){return this.getOption("scrollSpeed")},this.setDragDelay=function(e){this.setOption("dragDelay",e)},this.getDragDelay=function(){return this.getOption("dragDelay")},this.setSelectionStyle=function(e){this.setOption("selectionStyle",e)},this.getSelectionStyle=function(){return this.getOption("selectionStyle")},this.setHighlightActiveLine=function(e){this.setOption("highlightActiveLine",e)},this.getHighlightActiveLine=function(){return this.getOption("highlightActiveLine")},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.setHighlightSelectedWord=function(e){this.setOption("highlightSelectedWord",e)},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setAnimatedScroll=function(e){this.renderer.setAnimatedScroll(e)},this.getAnimatedScroll=function(){return this.renderer.getAnimatedScroll()},this.setShowInvisibles=function(e){this.renderer.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setDisplayIndentGuides=function(e){this.renderer.setDisplayIndentGuides(e)},this.getDisplayIndentGuides=function(){return this.renderer.getDisplayIndentGuides()},this.setHighlightIndentGuides=function(e){this.renderer.setHighlightIndentGuides(e)},this.getHighlightIndentGuides=function(){return this.renderer.getHighlightIndentGuides()},this.setShowPrintMargin=function(e){this.renderer.setShowPrintMargin(e)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(e){this.renderer.setPrintMarginColumn(e)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.setReadOnly=function(e){this.setOption("readOnly",e)},this.getReadOnly=function(){return this.getOption("readOnly")},this.setBehavioursEnabled=function(e){this.setOption("behavioursEnabled",e)},this.getBehavioursEnabled=function(){return this.getOption("behavioursEnabled")},this.setWrapBehavioursEnabled=function(e){this.setOption("wrapBehavioursEnabled",e)},this.getWrapBehavioursEnabled=function(){return this.getOption("wrapBehavioursEnabled")},this.setShowFoldWidgets=function(e){this.setOption("showFoldWidgets",e)},this.getShowFoldWidgets=function(){return this.getOption("showFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.remove=function(e){this.selection.isEmpty()&&(e=="left"?this.selection.selectLeft():this.selection.selectRight());var t=this.getSelectionRange();if(this.getBehavioursEnabled()){var n=this.session,r=n.getState(t.start.row),i=n.getMode().transformAction(r,"deletion",this,n,t);if(t.end.column===0){var s=n.getTextRange(t);if(s[s.length-1]=="\n"){var o=n.getLine(t.end.row);/^\s+$/.test(o)&&(t.end.column=o.length)}}i&&(t=i)}this.session.remove(t),this.clearSelection()},this.removeWordRight=function(){this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeWordLeft=function(){this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineStart=function(){this.selection.isEmpty()&&this.selection.selectLineStart(),this.selection.isEmpty()&&this.selection.selectLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineEnd=function(){this.selection.isEmpty()&&this.selection.selectLineEnd();var e=this.getSelectionRange();e.start.column==e.end.column&&e.start.row==e.end.row&&(e.end.column=0,e.end.row++),this.session.remove(e),this.clearSelection()},this.splitLine=function(){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var e=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(e)},this.setGhostText=function(e,t){this.session.widgetManager||(this.session.widgetManager=new w(this.session),this.session.widgetManager.attach(this)),this.renderer.setGhostText(e,t)},this.removeGhostText=function(){if(!this.session.widgetManager)return;this.renderer.removeGhostText()},this.transposeLetters=function(){if(!this.selection.isEmpty())return;var e=this.getCursorPosition(),t=e.column;if(t===0)return;var n=this.session.getLine(e.row),r,i;tt.toLowerCase()?1:0});var i=new d(0,0,0,0);for(var r=e.first;r<=e.last;r++){var s=t.getLine(r);i.start.row=r,i.end.row=r,i.end.column=s.length,t.replace(i,n[r-e.first])}},this.toggleCommentLines=function(){var e=this.session.getState(this.getCursorPosition().row),t=this.$getSelectedRows();this.session.getMode().toggleCommentLines(e,this.session,t.first,t.last)},this.toggleBlockComment=function(){var e=this.getCursorPosition(),t=this.session.getState(e.row),n=this.getSelectionRange();this.session.getMode().toggleBlockComment(t,this.session,n,e)},this.getNumberAt=function(e,t){var n=/[\-]?[0-9]+(?:\.[0-9]+)?/g;n.lastIndex=0;var r=this.session.getLine(e);while(n.lastIndex=t){var s={value:i[0],start:i.index,end:i.index+i[0].length};return s}}return null},this.modifyNumber=function(e){var t=this.selection.getCursor().row,n=this.selection.getCursor().column,r=new d(t,n-1,t,n),i=this.session.getTextRange(r);if(!isNaN(parseFloat(i))&&isFinite(i)){var s=this.getNumberAt(t,n);if(s){var o=s.value.indexOf(".")>=0?s.start+s.value.indexOf(".")+1:s.end,u=s.start+s.value.length-o,a=parseFloat(s.value);a*=Math.pow(10,u),o!==s.end&&n=u&&s<=a&&(n=t,f.selection.clearSelection(),f.moveCursorTo(e,u+r),f.selection.selectTo(e,a+r)),u=a});var l=this.$toggleWordPairs,c;for(var h=0;h=a&&u<=f&&p.match(/((?:https?|ftp):\/\/[\S]+)/)){l=p.replace(/[\s:.,'";}\]]+$/,"");break}a=f}}catch(d){n={error:d}}finally{try{h&&!h.done&&(i=c.return)&&i.call(c)}finally{if(n)throw n.error}}return l},this.openLink=function(){var e=this.selection.getCursor(),t=this.findLinkAt(e.row,e.column);return t&&window.open(t,"_blank"),t!=null},this.removeLines=function(){var e=this.$getSelectedRows();this.session.removeFullLines(e.first,e.last),this.clearSelection()},this.duplicateSelection=function(){var e=this.selection,t=this.session,n=e.getRange(),r=e.isBackwards();if(n.isEmpty()){var i=n.start.row;t.duplicateLines(i,i)}else{var s=r?n.start:n.end,o=t.insert(s,t.getTextRange(n),!1);n.start=s,n.end=o,e.setSelectionRange(n,r)}},this.moveLinesDown=function(){this.$moveLines(1,!1)},this.moveLinesUp=function(){this.$moveLines(-1,!1)},this.moveText=function(e,t,n){return this.session.moveText(e,t,n)},this.copyLinesUp=function(){this.$moveLines(-1,!0)},this.copyLinesDown=function(){this.$moveLines(1,!0)},this.$moveLines=function(e,t){var n,r,i=this.selection;if(!i.inMultiSelectMode||this.inVirtualSelectionMode){var s=i.toOrientedRange();n=this.$getSelectedRows(s),r=this.session.$moveLines(n.first,n.last,t?0:e),t&&e==-1&&(r=0),s.moveBy(r,0),i.fromOrientedRange(s)}else{var o=i.rangeList.ranges;i.rangeList.detach(this.session),this.inVirtualSelectionMode=!0;var u=0,a=0,f=o.length;for(var l=0;lp+1)break;p=d.last}l--,u=this.session.$moveLines(h,p,t?0:e),t&&e==-1&&(c=l+1);while(c<=l)o[c].moveBy(u,0),c++;t||(u=0),a+=u}i.fromOrientedRange(i.ranges[0]),i.rangeList.attach(this.session),this.inVirtualSelectionMode=!1}},this.$getSelectedRows=function(e){return e=(e||this.getSelectionRange()).collapseRows(),{first:this.session.getRowFoldStart(e.start.row),last:this.session.getRowFoldEnd(e.end.row)}},this.onCompositionStart=function(e){this.renderer.showComposition(e)},this.onCompositionUpdate=function(e){this.renderer.setCompositionText(e)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(e){return e>=this.getFirstVisibleRow()&&e<=this.getLastVisibleRow()},this.isRowFullyVisible=function(e){return e>=this.renderer.getFirstFullyVisibleRow()&&e<=this.renderer.getLastFullyVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$moveByPage=function(e,t){var n=this.renderer,r=this.renderer.layerConfig,i=e*Math.floor(r.height/r.lineHeight);t===!0?this.selection.$moveSelection(function(){this.moveCursorBy(i,0)}):t===!1&&(this.selection.moveCursorBy(i,0),this.selection.clearSelection());var s=n.scrollTop;n.scrollBy(0,i*r.lineHeight),t!=null&&n.scrollCursorIntoView(null,.5),n.animateScrolling(s)},this.selectPageDown=function(){this.$moveByPage(1,!0)},this.selectPageUp=function(){this.$moveByPage(-1,!0)},this.gotoPageDown=function(){this.$moveByPage(1,!1)},this.gotoPageUp=function(){this.$moveByPage(-1,!1)},this.scrollPageDown=function(){this.$moveByPage(1)},this.scrollPageUp=function(){this.$moveByPage(-1)},this.scrollToRow=function(e){this.renderer.scrollToRow(e)},this.scrollToLine=function(e,t,n,r){this.renderer.scrollToLine(e,t,n,r)},this.centerSelection=function(){var e=this.getSelectionRange(),t={row:Math.floor(e.start.row+(e.end.row-e.start.row)/2),column:Math.floor(e.start.column+(e.end.column-e.start.column)/2)};this.renderer.alignCursor(t,.5)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.selection.selectAll()},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(e,t){this.selection.moveCursorTo(e,t)},this.moveCursorToPosition=function(e){this.selection.moveCursorToPosition(e)},this.jumpToMatching=function(e,t){var n=this.getCursorPosition(),r=new b(this.session,n.row,n.column),i=r.getCurrentToken(),s=0;i&&i.type.indexOf("tag-name")!==-1&&(i=r.stepBackward());var o=i||r.stepForward();if(!o)return;var u,a=!1,f={},l=n.column-o.start,c,h={")":"(","(":"(","]":"[","[":"[","{":"{","}":"{"};do{if(o.value.match(/[{}()\[\]]/g))for(;l1?f[o.value]++:i.value==="=0;--s)this.$tryReplace(n[s],e)&&r++;return this.selection.setSelectionRange(i),r},this.$tryReplace=function(e,t){var n=this.session.getTextRange(e);return t=this.$search.replace(n,t),t!==null?(e.end=this.session.replace(e,t),e):null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(e,t,n){t||(t={}),typeof e=="string"||e instanceof RegExp?t.needle=e:typeof e=="object"&&i.mixin(t,e);var r=this.selection.getRange();t.needle==null&&(e=this.session.getTextRange(r)||this.$search.$options.needle,e||(r=this.session.getWordRange(r.start.row,r.start.column),e=this.session.getTextRange(r)),this.$search.set({needle:e})),this.$search.set(t),t.start||this.$search.set({start:r});var s=this.$search.find(this.session);if(t.preventScroll)return s;if(s)return this.revealRange(s,n),s;t.backwards?r.start=r.end:r.end=r.start,this.selection.setRange(r)},this.findNext=function(e,t){this.find({skipCurrent:!0,backwards:!1},e,t)},this.findPrevious=function(e,t){this.find(e,{skipCurrent:!0,backwards:!0},t)},this.revealRange=function(e,t){this.session.unfold(e),this.selection.setSelectionRange(e);var n=this.renderer.scrollTop;this.renderer.scrollSelectionIntoView(e.start,e.end,.5),t!==!1&&this.renderer.animateScrolling(n)},this.undo=function(){this.session.getUndoManager().undo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.redo=function(){this.session.getUndoManager().redo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.destroy=function(){this.$toDestroy&&(this.$toDestroy.forEach(function(e){e.destroy()}),this.$toDestroy=null),this.$mouseHandler&&this.$mouseHandler.destroy(),this.renderer.destroy(),this._signal("destroy",this),this.session&&this.session.destroy(),this._$emitInputEvent&&this._$emitInputEvent.cancel(),this.removeAllListeners()},this.setAutoScrollEditorIntoView=function(e){if(!e)return;var t,n=this,r=!1;this.$scrollAnchor||(this.$scrollAnchor=document.createElement("div"));var i=this.$scrollAnchor;i.style.cssText="position:absolute",this.container.insertBefore(i,this.container.firstChild);var s=this.on("changeSelection",function(){r=!0}),o=this.renderer.on("beforeRender",function(){r&&(t=n.renderer.container.getBoundingClientRect())}),u=this.renderer.on("afterRender",function(){if(r&&t&&(n.isFocused()||n.searchBox&&n.searchBox.isFocused())){var e=n.renderer,s=e.$cursorLayer.$pixelPos,o=e.layerConfig,u=s.top-o.offset;s.top>=0&&u+t.top<0?r=!0:s.topwindow.innerHeight?r=!1:r=null,r!=null&&(i.style.top=u+"px",i.style.left=s.left+"px",i.style.height=o.lineHeight+"px",i.scrollIntoView(r)),r=t=null}});this.setAutoScrollEditorIntoView=function(e){if(e)return;delete this.setAutoScrollEditorIntoView,this.off("changeSelection",s),this.renderer.off("afterRender",u),this.renderer.off("beforeRender",o)}},this.$resetCursorStyle=function(){var e=this.$cursorStyle||"ace",t=this.renderer.$cursorLayer;if(!t)return;t.setSmoothBlinking(/smooth/.test(e)),t.isBlinking=!this.$readOnly&&e!="wide",s.setCssClass(t.element,"ace_slim-cursors",/slim/.test(e))},this.prompt=function(e,t,n){var r=this;y.loadModule("./ext/prompt",function(i){i.prompt(r,e,t,n)})}}.call(S.prototype),y.defineOptions(S.prototype,"editor",{selectionStyle:{set:function(e){this.onSelectionChange(),this._signal("changeSelectionStyle",{data:e})},initialValue:"line"},highlightActiveLine:{set:function(){this.$updateHighlightActiveLine()},initialValue:!0},highlightSelectedWord:{set:function(e){this.$onSelectionChange()},initialValue:!0},readOnly:{set:function(e){this.textInput.setReadOnly(e),this.$resetCursorStyle()},initialValue:!1},copyWithEmptySelection:{set:function(e){this.textInput.setCopyWithEmptySelection(e)},initialValue:!1},cursorStyle:{set:function(e){this.$resetCursorStyle()},values:["ace","slim","smooth","wide"],initialValue:"ace"},mergeUndoDeltas:{values:[!1,!0,"always"],initialValue:!0},behavioursEnabled:{initialValue:!0},wrapBehavioursEnabled:{initialValue:!0},enableAutoIndent:{initialValue:!0},autoScrollEditorIntoView:{set:function(e){this.setAutoScrollEditorIntoView(e)}},keyboardHandler:{set:function(e){this.setKeyboardHandler(e)},get:function(){return this.$keybindingId},handlesSet:!0},value:{set:function(e){this.session.setValue(e)},get:function(){return this.getValue()},handlesSet:!0,hidden:!0},session:{set:function(e){this.setSession(e)},get:function(){return this.session},handlesSet:!0,hidden:!0},showLineNumbers:{set:function(e){this.renderer.$gutterLayer.setShowLineNumbers(e),this.renderer.$loop.schedule(this.renderer.CHANGE_GUTTER),e&&this.$relativeLineNumbers?x.attach(this):x.detach(this)},initialValue:!0},relativeLineNumbers:{set:function(e){this.$showLineNumbers&&e?x.attach(this):x.detach(this)}},placeholder:{set:function(e){this.$updatePlaceholder||(this.$updatePlaceholder=function(){var e=this.session&&(this.renderer.$composition||this.getValue());if(e&&this.renderer.placeholderNode)this.renderer.off("afterRender",this.$updatePlaceholder),s.removeCssClass(this.container,"ace_hasPlaceholder"),this.renderer.placeholderNode.remove(),this.renderer.placeholderNode=null;else if(!e&&!this.renderer.placeholderNode){this.renderer.on("afterRender",this.$updatePlaceholder),s.addCssClass(this.container,"ace_hasPlaceholder");var t=s.createElement("div");t.className="ace_placeholder",t.textContent=this.$placeholder||"",this.renderer.placeholderNode=t,this.renderer.content.appendChild(this.renderer.placeholderNode)}else!e&&this.renderer.placeholderNode&&(this.renderer.placeholderNode.textContent=this.$placeholder||"")}.bind(this),this.on("input",this.$updatePlaceholder)),this.$updatePlaceholder()}},customScrollbar:"renderer",hScrollBarAlwaysVisible:"renderer",vScrollBarAlwaysVisible:"renderer",highlightGutterLine:"renderer",animatedScroll:"renderer",showInvisibles:"renderer",showPrintMargin:"renderer",printMarginColumn:"renderer",printMargin:"renderer",fadeFoldWidgets:"renderer",showFoldWidgets:"renderer",displayIndentGuides:"renderer",highlightIndentGuides:"renderer",showGutter:"renderer",fontSize:"renderer",fontFamily:"renderer",maxLines:"renderer",minLines:"renderer",scrollPastEnd:"renderer",fixedWidthGutter:"renderer",theme:"renderer",hasCssTransforms:"renderer",maxPixelHeight:"renderer",useTextareaForIME:"renderer",scrollSpeed:"$mouseHandler",dragDelay:"$mouseHandler",dragEnabled:"$mouseHandler",focusTimeout:"$mouseHandler",tooltipFollowsMouse:"$mouseHandler",firstLineNumber:"session",overwrite:"session",newLineMode:"session",useWorker:"session",useSoftTabs:"session",navigateWithinSoftTabs:"session",tabSize:"session",wrap:"session",indentedSoftWrap:"session",foldStyle:"session",mode:"session"});var x={getText:function(e,t){return(Math.abs(e.selection.lead.row-t)||t+1+(t<9?"\u00b7":""))+""},getWidth:function(e,t,n){return Math.max(t.toString().length,(n.lastRow+1).toString().length,2)*n.characterWidth},update:function(e,t){t.renderer.$loop.schedule(t.renderer.CHANGE_GUTTER)},attach:function(e){e.renderer.$gutterLayer.$renderer=this,e.on("changeSelection",this.update),this.update(null,e)},detach:function(e){e.renderer.$gutterLayer.$renderer==this&&(e.renderer.$gutterLayer.$renderer=null),e.off("changeSelection",this.update),this.update(null,e)}};t.Editor=S}),ace.define("ace/undomanager",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){for(var n=t;n--;){var r=e[n];if(r&&!r[0].ignore){while(n0){a.row+=i,a.column+=a.row==r.row?s:0;continue}!t&&l<=0&&(a.row=n.row,a.column=n.column,l===0&&(a.bias=1))}}function f(e){return{row:e.row,column:e.column}}function l(e){return{start:f(e.start),end:f(e.end),action:e.action,lines:e.lines.slice()}}function c(e){e=e||this;if(Array.isArray(e))return e.map(c).join("\n");var t="";e.action?(t=e.action=="insert"?"+":"-",t+="["+e.lines+"]"):e.value&&(Array.isArray(e.value)?t=e.value.map(h).join("\n"):t=h(e.value)),e.start&&(t+=h(e));if(e.id||e.rev)t+=" ("+(e.id||e.rev)+")";return t}function h(e){return e.start.row+":"+e.start.column+"=>"+e.end.row+":"+e.end.column}function p(e,t){var n=e.action=="insert",r=t.action=="insert";if(n&&r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}else if(!n&&r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(!n&&!r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}return[t,e]}function d(e,t){for(var n=e.length;n--;)for(var r=0;r=0?m(e,t,-1):o(e.start,t.start)<=0?m(t,e,1):(m(e,s.fromPoints(t.start,e.start),-1),m(t,e,1));else if(!n&&r)o(t.start,e.end)>=0?m(t,e,-1):o(t.start,e.start)<=0?m(e,t,1):(m(t,s.fromPoints(e.start,t.start),-1),m(e,t,1));else if(!n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0)){var i,u;return o(e.start,t.start)<0&&(i=e,e=y(e,t.start)),o(e.end,t.end)>0&&(u=y(e,t.end)),g(t.end,e.start,e.end,-1),u&&!i&&(e.lines=u.lines,e.start=u.start,e.end=u.end,u=e),[t,i,u].filter(Boolean)}m(e,t,-1)}return[t,e]}function m(e,t,n){g(e.start,t.start,t.end,n),g(e.end,t.start,t.end,n)}function g(e,t,n,r){e.row==(r==1?t:n).row&&(e.column+=r*(n.column-t.column)),e.row+=r*(n.row-t.row)}function y(e,t){var n=e.lines,r=e.end;e.end=f(t);var i=e.end.row-e.start.row,s=n.splice(i,n.length),o=i?t.column:t.column-e.start.column;n.push(s[0].substring(0,o)),s[0]=s[0].substr(o);var u={start:f(t),end:r,lines:s,action:e.action};return u}function b(e,t){t=l(t);for(var n=e.length;n--;){var r=e[n];for(var i=0;ithis.$undoDepth-1&&this.$undoStack.splice(0,r-this.$undoDepth+1),this.$undoStack.push(this.lastDeltas),e.id=this.$rev=++this.$maxRev}if(e.action=="remove"||e.action=="insert")this.$lastDelta=e;this.lastDeltas.push(e)},this.addSelection=function(e,t){this.selections.push({value:e,rev:t||this.$rev})},this.startNewGroup=function(){return this.lastDeltas=null,this.$rev},this.markIgnored=function(e,t){t==null&&(t=this.$rev+1);var n=this.$undoStack;for(var r=n.length;r--;){var i=n[r][0];if(i.id<=e)break;i.id0},this.canRedo=function(){return this.$redoStack.length>0},this.bookmark=function(e){e==undefined&&(e=this.$rev),this.mark=e},this.isAtBookmark=function(){return this.$rev===this.mark},this.toJSON=function(){},this.fromJSON=function(){},this.hasUndo=this.canUndo,this.hasRedo=this.canRedo,this.isClean=this.isAtBookmark,this.markClean=this.bookmark,this.$prettyPrint=function(e){return e?c(e):c(this.$undoStack)+"\n---\n"+c(this.$redoStack)}}).call(r.prototype);var s=e("./range").Range,o=s.comparePoints,u=s.comparePoints;t.UndoManager=r}),ace.define("ace/layer/lines",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e,t){this.element=e,this.canvasHeight=t||5e5,this.element.style.height=this.canvasHeight*2+"px",this.cells=[],this.cellCache=[],this.$offsetCoefficient=0};(function(){this.moveContainer=function(e){r.translate(this.element,0,-(e.firstRowScreen*e.lineHeight%this.canvasHeight)-e.offset*this.$offsetCoefficient)},this.pageChanged=function(e,t){return Math.floor(e.firstRowScreen*e.lineHeight/this.canvasHeight)!==Math.floor(t.firstRowScreen*t.lineHeight/this.canvasHeight)},this.computeLineTop=function(e,t,n){var r=t.firstRowScreen*t.lineHeight,i=Math.floor(r/this.canvasHeight),s=n.documentToScreenRow(e,0)*t.lineHeight;return s-i*this.canvasHeight},this.computeLineHeight=function(e,t,n){return t.lineHeight*n.getRowLineCount(e)},this.getLength=function(){return this.cells.length},this.get=function(e){return this.cells[e]},this.shift=function(){this.$cacheCell(this.cells.shift())},this.pop=function(){this.$cacheCell(this.cells.pop())},this.push=function(e){if(Array.isArray(e)){this.cells.push.apply(this.cells,e);var t=r.createFragment(this.element);for(var n=0;ns&&(a=i.end.row+1,i=t.getNextFoldLine(a,i),s=i?i.start.row:Infinity);if(a>r){while(this.$lines.getLength()>u+1)this.$lines.pop();break}o=this.$lines.get(++u),o?o.row=a:(o=this.$lines.createCell(a,e,this.session,f),this.$lines.push(o)),this.$renderCell(o,e,i,a),a++}this._signal("afterRender"),this.$updateGutterWidth(e)},this.$updateGutterWidth=function(e){var t=this.session,n=t.gutterRenderer||this.$renderer,r=t.$firstLineNumber,i=this.$lines.last()?this.$lines.last().text:"";if(this.$fixedWidth||t.$useWrapMode)i=t.getLength()+r-1;var s=n?n.getWidth(t,i,e):i.toString().length*e.characterWidth,o=this.$padding||this.$computePadding();s+=o.left+o.right,s!==this.gutterWidth&&!isNaN(s)&&(this.gutterWidth=s,this.element.parentNode.style.width=this.element.style.width=Math.ceil(this.gutterWidth)+"px",this._signal("changeGutterWidth",s))},this.$updateCursorRow=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.getCursor();if(this.$cursorRow===e.row)return;this.$cursorRow=e.row},this.updateLineHighlight=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.cursor.row;this.$cursorRow=e;if(this.$cursorCell&&this.$cursorCell.row==e)return;this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ",""));var t=this.$lines.cells;this.$cursorCell=null;for(var n=0;n=this.$cursorRow){if(r.row>this.$cursorRow){var i=this.session.getFoldLine(this.$cursorRow);if(!(n>0&&i&&i.start.row==t[n-1].row))break;r=t[n-1]}r.element.className="ace_gutter-active-line "+r.element.className,this.$cursorCell=r;break}}},this.scrollLines=function(e){var t=this.config;this.config=e,this.$updateCursorRow();if(this.$lines.pageChanged(t,e))return this.update(e);this.$lines.moveContainer(e);var n=Math.min(e.lastRow+e.gutterOffset,this.session.getLength()-1),r=this.oldLastRow;this.oldLastRow=n;if(!t||r0;i--)this.$lines.shift();if(r>n)for(var i=this.session.getFoldedRowCount(n+1,r);i>0;i--)this.$lines.pop();e.firstRowr&&this.$lines.push(this.$renderLines(e,r+1,n)),this.updateLineHighlight(),this._signal("afterRender"),this.$updateGutterWidth(e)},this.$renderLines=function(e,t,n){var r=[],i=t,s=this.session.getNextFoldLine(i),o=s?s.start.row:Infinity;for(;;){i>o&&(i=s.end.row+1,s=this.session.getNextFoldLine(i,s),o=s?s.start.row:Infinity);if(i>n)break;var u=this.$lines.createCell(i,e,this.session,f);this.$renderCell(u,e,s,i),r.push(u),i++}return r},this.$renderCell=function(e,t,n,i){var s=e.element,o=this.session,u=s.childNodes[0],a=s.childNodes[1],f=o.$firstLineNumber,l=o.$breakpoints,c=o.$decorations,h=o.gutterRenderer||this.$renderer,p=this.$showFoldWidgets&&o.foldWidgets,d=n?n.start.row:Number.MAX_VALUE,v="ace_gutter-cell ";this.$highlightGutterLine&&(i==this.$cursorRow||n&&i=d&&this.$cursorRow<=n.end.row)&&(v+="ace_gutter-active-line ",this.$cursorCell!=e&&(this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ","")),this.$cursorCell=e)),l[i]&&(v+=l[i]),c[i]&&(v+=c[i]),this.$annotations[i]&&(v+=this.$annotations[i].className),s.className!=v&&(s.className=v);if(p){var m=p[i];m==null&&(m=p[i]=o.getFoldWidget(i))}if(m){var v="ace_fold-widget ace_"+m;m=="start"&&i==d&&in.right-t.right)return"foldWidgets"}}).call(a.prototype),t.Gutter=a}),ace.define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../range").Range,i=e("../lib/dom"),s=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_marker-layer",e.appendChild(this.element)};(function(){function e(e,t,n,r){return(e?1:0)|(t?2:0)|(n?4:0)|(r?8:0)}this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setMarkers=function(e){this.markers=e},this.elt=function(e,t){var n=this.i!=-1&&this.element.childNodes[this.i];n?this.i++:(n=document.createElement("div"),this.element.appendChild(n),this.i=-1),n.style.cssText=t,n.className=e},this.update=function(e){if(!e)return;this.config=e,this.i=0;var t;for(var n in this.markers){var r=this.markers[n];if(!r.range){r.update(t,this,this.session,e);continue}var i=r.range.clipRows(e.firstRow,e.lastRow);if(i.isEmpty())continue;i=i.toScreenRange(this.session);if(r.renderer){var s=this.$getTop(i.start.row,e),o=this.$padding+i.start.column*e.characterWidth;r.renderer(t,i,o,s,e)}else r.type=="fullLine"?this.drawFullLineMarker(t,i,r.clazz,e):r.type=="screenLine"?this.drawScreenLineMarker(t,i,r.clazz,e):i.isMultiLine()?r.type=="text"?this.drawTextMarker(t,i,r.clazz,e):this.drawMultiLineMarker(t,i,r.clazz,e):this.drawSingleLineMarker(t,i,r.clazz+" ace_start"+" ace_br15",e)}if(this.i!=-1)while(this.ip,l==f),s,l==f?0:1,o)},this.drawMultiLineMarker=function(e,t,n,r,i){var s=this.$padding,o=r.lineHeight,u=this.$getTop(t.start.row,r),a=s+t.start.column*r.characterWidth;i=i||"";if(this.session.$bidiHandler.isBidiRow(t.start.row)){var f=t.clone();f.end.row=f.start.row,f.end.column=this.session.getLine(f.start.row).length,this.drawBidiSingleLineMarker(e,f,n+" ace_br1 ace_start",r,null,i)}else this.elt(n+" ace_br1 ace_start","height:"+o+"px;"+"right:0;"+"top:"+u+"px;left:"+a+"px;"+(i||""));if(this.session.$bidiHandler.isBidiRow(t.end.row)){var f=t.clone();f.start.row=f.end.row,f.start.column=0,this.drawBidiSingleLineMarker(e,f,n+" ace_br12",r,null,i)}else{u=this.$getTop(t.end.row,r);var l=t.end.column*r.characterWidth;this.elt(n+" ace_br12","height:"+o+"px;"+"width:"+l+"px;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))}o=(t.end.row-t.start.row-1)*r.lineHeight;if(o<=0)return;u=this.$getTop(t.start.row+1,r);var c=(t.start.column?1:0)|(t.end.column?0:8);this.elt(n+(c?" ace_br"+c:""),"height:"+o+"px;"+"right:0;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))},this.drawSingleLineMarker=function(e,t,n,r,i,s){if(this.session.$bidiHandler.isBidiRow(t.start.row))return this.drawBidiSingleLineMarker(e,t,n,r,i,s);var o=r.lineHeight,u=(t.end.column+(i||0)-t.start.column)*r.characterWidth,a=this.$getTop(t.start.row,r),f=this.$padding+t.start.column*r.characterWidth;this.elt(n,"height:"+o+"px;"+"width:"+u+"px;"+"top:"+a+"px;"+"left:"+f+"px;"+(s||""))},this.drawBidiSingleLineMarker=function(e,t,n,r,i,s){var o=r.lineHeight,u=this.$getTop(t.start.row,r),a=this.$padding,f=this.session.$bidiHandler.getSelections(t.start.column,t.end.column);f.forEach(function(e){this.elt(n,"height:"+o+"px;"+"width:"+(e.width+(i||0))+"px;"+"top:"+u+"px;"+"left:"+(a+e.left)+"px;"+(s||""))},this)},this.drawFullLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;t.start.row!=t.end.row&&(o+=this.$getTop(t.end.row,r)-s),this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))},this.drawScreenLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))}}).call(s.prototype),t.Marker=s}),ace.define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/layer/lines","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("./lines").Lines,u=e("../lib/event_emitter").EventEmitter,a=function(e){this.dom=i,this.element=this.dom.createElement("div"),this.element.className="ace_layer ace_text-layer",e.appendChild(this.element),this.$updateEolChar=this.$updateEolChar.bind(this),this.$lines=new o(this.element)};(function(){r.implement(this,u),this.EOF_CHAR="\u00b6",this.EOL_CHAR_LF="\u00ac",this.EOL_CHAR_CRLF="\u00a4",this.EOL_CHAR=this.EOL_CHAR_LF,this.TAB_CHAR="\u2014",this.SPACE_CHAR="\u00b7",this.$padding=0,this.MAX_LINE_LENGTH=1e4,this.MAX_CHUNK_LENGTH=250,this.$updateEolChar=function(){var e=this.session.doc,t=e.getNewLineCharacter()=="\n"&&e.getNewLineMode()!="windows",n=t?this.EOL_CHAR_LF:this.EOL_CHAR_CRLF;if(this.EOL_CHAR!=n)return this.EOL_CHAR=n,!0},this.setPadding=function(e){this.$padding=e,this.element.style.margin="0 "+e+"px"},this.getLineHeight=function(){return this.$fontMetrics.$characterSize.height||0},this.getCharacterWidth=function(){return this.$fontMetrics.$characterSize.width||0},this.$setFontMetrics=function(e){this.$fontMetrics=e,this.$fontMetrics.on("changeCharacterSize",function(e){this._signal("changeCharacterSize",e)}.bind(this)),this.$pollSizeChanges()},this.checkForSizeChanges=function(){this.$fontMetrics.checkForSizeChanges()},this.$pollSizeChanges=function(){return this.$pollSizeChangesTimer=this.$fontMetrics.$pollSizeChanges()},this.setSession=function(e){this.session=e,e&&this.$computeTabString()},this.showInvisibles=!1,this.showSpaces=!1,this.showTabs=!1,this.showEOL=!1,this.setShowInvisibles=function(e){return this.showInvisibles==e?!1:(this.showInvisibles=e,typeof e=="string"?(this.showSpaces=/tab/i.test(e),this.showTabs=/space/i.test(e),this.showEOL=/eol/i.test(e)):this.showSpaces=this.showTabs=this.showEOL=e,this.$computeTabString(),!0)},this.displayIndentGuides=!0,this.setDisplayIndentGuides=function(e){return this.displayIndentGuides==e?!1:(this.displayIndentGuides=e,this.$computeTabString(),!0)},this.$highlightIndentGuides=!0,this.setHighlightIndentGuides=function(e){return this.$highlightIndentGuides===e?!1:(this.$highlightIndentGuides=e,e)},this.$tabStrings=[],this.onChangeTabSize=this.$computeTabString=function(){var e=this.session.getTabSize();this.tabSize=e;var t=this.$tabStrings=[0];for(var n=1;nl&&(u=a.end.row+1,a=this.session.getNextFoldLine(u,a),l=a?a.start.row:Infinity);if(u>i)break;var c=s[o++];if(c){this.dom.removeChildren(c),this.$renderLine(c,u,u==l?a:!1),f&&(c.style.top=this.$lines.computeLineTop(u,e,this.session)+"px");var h=e.lineHeight*this.session.getRowLength(u)+"px";c.style.height!=h&&(f=!0,c.style.height=h)}u++}if(f)while(o0;i--)this.$lines.shift();if(t.lastRow>e.lastRow)for(var i=this.session.getFoldedRowCount(e.lastRow+1,t.lastRow);i>0;i--)this.$lines.pop();e.firstRowt.lastRow&&this.$lines.push(this.$renderLinesFragment(e,t.lastRow+1,e.lastRow)),this.$highlightIndentGuide()},this.$renderLinesFragment=function(e,t,n){var r=[],s=t,o=this.session.getNextFoldLine(s),u=o?o.start.row:Infinity;for(;;){s>u&&(s=o.end.row+1,o=this.session.getNextFoldLine(s,o),u=o?o.start.row:Infinity);if(s>n)break;var a=this.$lines.createCell(s,e,this.session),f=a.element;this.dom.removeChildren(f),i.setStyle(f.style,"height",this.$lines.computeLineHeight(s,e,this.session)+"px"),i.setStyle(f.style,"top",this.$lines.computeLineTop(s,e,this.session)+"px"),this.$renderLine(f,s,s==u?o:!1),this.$useLineGroups()?f.className="ace_line_group":(f.className="ace_line",f.setAttribute("role","option")),r.push(a),s++}return r},this.update=function(e){this.$lines.moveContainer(e),this.config=e;var t=e.firstRow,n=e.lastRow,r=this.$lines;while(r.getLength())r.pop();r.push(this.$renderLinesFragment(e,t,n))},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderTokenInChunks=function(e,t,n,r){var i;for(var s=0;s=n)return t;if(t[0]==" "){r-=r%this.tabSize;var i=r/this.tabSize;for(var s=0;ss[o].start.row?this.$highlightIndentGuideMarker.dir=-1:this.$highlightIndentGuideMarker.dir=1;break}}if(!this.$highlightIndentGuideMarker.end&&e[t.row]!==""&&t.column===e[t.row].length){this.$highlightIndentGuideMarker.dir=1;for(var o=t.row+1;o0)for(var i=0;i=this.$highlightIndentGuideMarker.start+1){if(r.row>=this.$highlightIndentGuideMarker.end)break;this.$setIndentGuideActive(r,t)}}else for(var n=e.length-1;n>=0;n--){var r=e[n];if(this.$highlightIndentGuideMarker.end&&r.row=o)u=this.$renderTokenInChunks(a,u,l,c.substring(0,o-r)),c=c.substring(o-r),r=o,a=this.$createLineElement(),e.appendChild(a),a.appendChild(this.dom.createTextNode(s.stringRepeat("\u00a0",n.indent),this.element)),i++,u=0,o=n[i]||Number.MAX_VALUE;c.length!=0&&(r+=c.length,u=this.$renderTokenInChunks(a,u,l,c))}}n[n.length-1]>this.MAX_LINE_LENGTH&&this.$renderOverflowMessage(a,u,null,"",!0)},this.$renderSimpleLine=function(e,t){var n=0;for(var r=0;rthis.MAX_LINE_LENGTH){this.$renderOverflowMessage(e,n,i,s);return}n=this.$renderTokenInChunks(e,n,i,s)}},this.$renderOverflowMessage=function(e,t,n,r,i){n&&this.$renderTokenInChunks(e,t,n,r.slice(0,this.MAX_LINE_LENGTH-t));var s=this.dom.createElement("span");s.className="ace_inline_button ace_keyword ace_toggle_wrap",s.textContent=i?"":"",e.appendChild(s)},this.$renderLine=function(e,t,n){!n&&n!=0&&(n=this.session.getFoldLine(t));if(n)var r=this.$getFoldLineTokens(t,n);else var r=this.session.getTokens(t);var i=e;if(r.length){var s=this.session.getRowSplitData(t);if(s&&s.length){this.$renderWrappedLine(e,r,s);var i=e.lastChild}else{var i=e;this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i)),this.$renderSimpleLine(i,r)}}else this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i));if(this.showEOL&&i){n&&(t=n.end.row);var o=this.dom.createElement("span");o.className="ace_invisible ace_invisible_eol",o.textContent=t==this.session.getLength()-1?this.EOF_CHAR:this.EOL_CHAR,i.appendChild(o)}},this.$getFoldLineTokens=function(e,t){function i(e,t,n){var i=0,s=0;while(s+e[i].value.lengthn-t&&(o=o.substring(0,n-t)),r.push({type:e[i].type,value:o}),s=t+o.length,i+=1}while(sn?r.push({type:e[i].type,value:o.substring(0,n-s)}):r.push(e[i]),s+=o.length,i+=1}}var n=this.session,r=[],s=n.getTokens(e);return t.walk(function(e,t,o,u,a){e!=null?r.push({type:"fold",value:e}):(a&&(s=n.getTokens(t)),s.length&&i(s,u,o))},t.end.row,this.session.getLine(t.end.row).length),r},this.$useLineGroups=function(){return this.session.getUseWrapMode()},this.destroy=function(){}}).call(a.prototype),t.Text=a}),ace.define("ace/layer/cursor",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_cursor-layer",e.appendChild(this.element),this.isVisible=!1,this.isBlinking=!0,this.blinkInterval=1e3,this.smoothBlinking=!1,this.cursors=[],this.cursor=this.addCursor(),r.addCssClass(this.element,"ace_hidden-cursors"),this.$updateCursors=this.$updateOpacity.bind(this)};(function(){this.$updateOpacity=function(e){var t=this.cursors;for(var n=t.length;n--;)r.setStyle(t[n].style,"opacity",e?"":"0")},this.$startCssAnimation=function(){var e=this.cursors;for(var t=e.length;t--;)e[t].style.animationDuration=this.blinkInterval+"ms";this.$isAnimating=!0,setTimeout(function(){this.$isAnimating&&r.addCssClass(this.element,"ace_animate-blinking")}.bind(this))},this.$stopCssAnimation=function(){this.$isAnimating=!1,r.removeCssClass(this.element,"ace_animate-blinking")},this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setBlinking=function(e){e!=this.isBlinking&&(this.isBlinking=e,this.restartTimer())},this.setBlinkInterval=function(e){e!=this.blinkInterval&&(this.blinkInterval=e,this.restartTimer())},this.setSmoothBlinking=function(e){e!=this.smoothBlinking&&(this.smoothBlinking=e,r.setCssClass(this.element,"ace_smooth-blinking",e),this.$updateCursors(!0),this.restartTimer())},this.addCursor=function(){var e=r.createElement("div");return e.className="ace_cursor",this.element.appendChild(e),this.cursors.push(e),e},this.removeCursor=function(){if(this.cursors.length>1){var e=this.cursors.pop();return e.parentNode.removeChild(e),e}},this.hideCursor=function(){this.isVisible=!1,r.addCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.showCursor=function(){this.isVisible=!0,r.removeCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.restartTimer=function(){var e=this.$updateCursors;clearInterval(this.intervalId),clearTimeout(this.timeoutId),this.$stopCssAnimation(),this.smoothBlinking&&(this.$isSmoothBlinking=!1,r.removeCssClass(this.element,"ace_smooth-blinking")),e(!0);if(!this.isBlinking||!this.blinkInterval||!this.isVisible){this.$stopCssAnimation();return}this.smoothBlinking&&(this.$isSmoothBlinking=!0,setTimeout(function(){this.$isSmoothBlinking&&r.addCssClass(this.element,"ace_smooth-blinking")}.bind(this)));if(r.HAS_CSS_ANIMATION)this.$startCssAnimation();else{var t=function(){this.timeoutId=setTimeout(function(){e(!1)},.6*this.blinkInterval)}.bind(this);this.intervalId=setInterval(function(){e(!0),t()},this.blinkInterval),t()}},this.getPixelPosition=function(e,t){if(!this.config||!this.session)return{left:0,top:0};e||(e=this.session.selection.getCursor());var n=this.session.documentToScreenPosition(e),r=this.$padding+(this.session.$bidiHandler.isBidiRow(n.row,e.row)?this.session.$bidiHandler.getPosLeft(n.column):n.column*this.config.characterWidth),i=(n.row-(t?this.config.firstRowScreen:0))*this.config.lineHeight;return{left:r,top:i}},this.isCursorInView=function(e,t){return e.top>=0&&e.tope.height+e.offset||o.top<0)&&n>1)continue;var u=this.cursors[i++]||this.addCursor(),a=u.style;this.drawCursor?this.drawCursor(u,o,e,t[n],this.session):this.isCursorInView(o,e)?(r.setStyle(a,"display","block"),r.translate(u,o.left,o.top),r.setStyle(a,"width",Math.round(e.characterWidth)+"px"),r.setStyle(a,"height",e.lineHeight+"px")):r.setStyle(a,"display","none")}while(this.cursors.length>i)this.removeCursor();var f=this.session.getOverwrite();this.$setOverwrite(f),this.$pixelPos=o,this.restartTimer()},this.drawCursor=null,this.$setOverwrite=function(e){e!=this.overwrite&&(this.overwrite=e,e?r.addCssClass(this.element,"ace_overwrite-cursors"):r.removeCssClass(this.element,"ace_overwrite-cursors"))},this.destroy=function(){clearInterval(this.intervalId),clearTimeout(this.timeoutId)}}).call(i.prototype),t.Cursor=i}),ace.define("ace/scrollbar",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/event"),o=e("./lib/event_emitter").EventEmitter,u=32768,a=function(e){this.element=i.createElement("div"),this.element.className="ace_scrollbar ace_scrollbar"+this.classSuffix,this.inner=i.createElement("div"),this.inner.className="ace_scrollbar-inner",this.inner.textContent="\u00a0",this.element.appendChild(this.inner),e.appendChild(this.element),this.setVisible(!1),this.skipEvent=!1,s.addListener(this.element,"scroll",this.onScroll.bind(this)),s.addListener(this.element,"mousedown",s.preventDefault)};(function(){r.implement(this,o),this.setVisible=function(e){this.element.style.display=e?"":"none",this.isVisible=e,this.coeff=1}}).call(a.prototype);var f=function(e,t){a.call(this,e),this.scrollTop=0,this.scrollHeight=0,t.$scrollbarWidth=this.width=i.scrollbarWidth(e.ownerDocument),this.inner.style.width=this.element.style.width=(this.width||15)+5+"px",this.$minWidth=0};r.inherits(f,a),function(){this.classSuffix="-v",this.onScroll=function(){if(!this.skipEvent){this.scrollTop=this.element.scrollTop;if(this.coeff!=1){var e=this.element.clientHeight/this.scrollHeight;this.scrollTop=this.scrollTop*(1-e)/(this.coeff-e)}this._emit("scroll",{data:this.scrollTop})}this.skipEvent=!1},this.getWidth=function(){return Math.max(this.isVisible?this.width:0,this.$minWidth||0)},this.setHeight=function(e){this.element.style.height=e+"px"},this.setInnerHeight=this.setScrollHeight=function(e){this.scrollHeight=e,e>u?(this.coeff=u/e,e=u):this.coeff!=1&&(this.coeff=1),this.inner.style.height=e+"px"},this.setScrollTop=function(e){this.scrollTop!=e&&(this.skipEvent=!0,this.scrollTop=e,this.element.scrollTop=e*this.coeff)}}.call(f.prototype);var l=function(e,t){a.call(this,e),this.scrollLeft=0,this.height=t.$scrollbarWidth,this.inner.style.height=this.element.style.height=(this.height||15)+5+"px"};r.inherits(l,a),function(){this.classSuffix="-h",this.onScroll=function(){this.skipEvent||(this.scrollLeft=this.element.scrollLeft,this._emit("scroll",{data:this.scrollLeft})),this.skipEvent=!1},this.getHeight=function(){return this.isVisible?this.height:0},this.setWidth=function(e){this.element.style.width=e+"px"},this.setInnerWidth=function(e){this.inner.style.width=e+"px"},this.setScrollWidth=function(e){this.inner.style.width=e+"px"},this.setScrollLeft=function(e){this.scrollLeft!=e&&(this.skipEvent=!0,this.scrollLeft=this.element.scrollLeft=e)}}.call(l.prototype),t.ScrollBar=f,t.ScrollBarV=f,t.ScrollBarH=l,t.VScrollBar=f,t.HScrollBar=l}),ace.define("ace/scrollbar_custom",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/event"),o=e("./lib/event_emitter").EventEmitter;i.importCssString(".ace_editor>.ace_sb-v div, .ace_editor>.ace_sb-h div{\n position: absolute;\n background: rgba(128, 128, 128, 0.6);\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n border: 1px solid #bbb;\n border-radius: 2px;\n z-index: 8;\n}\n.ace_editor>.ace_sb-v, .ace_editor>.ace_sb-h {\n position: absolute;\n z-index: 6;\n background: none;\n overflow: hidden!important;\n}\n.ace_editor>.ace_sb-v {\n z-index: 6;\n right: 0;\n top: 0;\n width: 12px;\n}\n.ace_editor>.ace_sb-v div {\n z-index: 8;\n right: 0;\n width: 100%;\n}\n.ace_editor>.ace_sb-h {\n bottom: 0;\n left: 0;\n height: 12px;\n}\n.ace_editor>.ace_sb-h div {\n bottom: 0;\n height: 100%;\n}\n.ace_editor>.ace_sb_grabbed {\n z-index: 8;\n background: #000;\n}","ace_scrollbar.css",!1);var u=function(e){this.element=i.createElement("div"),this.element.className="ace_sb"+this.classSuffix,this.inner=i.createElement("div"),this.inner.className="",this.element.appendChild(this.inner),this.VScrollWidth=12,this.HScrollHeight=12,e.appendChild(this.element),this.setVisible(!1),this.skipEvent=!1,s.addMultiMouseDownListener(this.element,[500,300,300],this,"onMouseDown")};(function(){r.implement(this,o),this.setVisible=function(e){this.element.style.display=e?"":"none",this.isVisible=e,this.coeff=1}}).call(u.prototype);var a=function(e,t){u.call(this,e),this.scrollTop=0,this.scrollHeight=0,this.parent=e,this.width=this.VScrollWidth,this.renderer=t,this.inner.style.width=this.element.style.width=(this.width||15)+"px",this.$minWidth=0};r.inherits(a,u),function(){this.classSuffix="-v",r.implement(this,o),this.onMouseDown=function(e,t){if(e!=="mousedown")return;if(s.getButton(t)!==0||t.detail===2)return;if(t.target===this.inner){var n=this,r=t.clientY,i=function(e){r=e.clientY},o=function(){clearInterval(l)},u=t.clientY,a=this.thumbTop,f=function(){if(r===undefined)return;var e=n.scrollTopFromThumbTop(a+r-u);if(e===n.scrollTop)return;n._emit("scroll",{data:e})};s.capture(this.inner,i,o);var l=setInterval(f,20);return s.preventDefault(t)}var c=t.clientY-this.element.getBoundingClientRect().top-this.thumbHeight/2;return this._emit("scroll",{data:this.scrollTopFromThumbTop(c)}),s.preventDefault(t)},this.getHeight=function(){return this.height},this.scrollTopFromThumbTop=function(e){var t=e*(this.pageHeight-this.viewHeight)/(this.slideHeight-this.thumbHeight);return t>>=0,t<0?t=0:t>this.pageHeight-this.viewHeight&&(t=this.pageHeight-this.viewHeight),t},this.getWidth=function(){return Math.max(this.isVisible?this.width:0,this.$minWidth||0)},this.setHeight=function(e){this.height=Math.max(0,e),this.slideHeight=this.height,this.viewHeight=this.height,this.setScrollHeight(this.pageHeight,!0)},this.setInnerHeight=this.setScrollHeight=function(e,t){if(this.pageHeight===e&&!t)return;this.pageHeight=e,this.thumbHeight=this.slideHeight*this.viewHeight/this.pageHeight,this.thumbHeight>this.slideHeight&&(this.thumbHeight=this.slideHeight),this.thumbHeight<15&&(this.thumbHeight=15),this.inner.style.height=this.thumbHeight+"px",this.scrollTop>this.pageHeight-this.viewHeight&&(this.scrollTop=this.pageHeight-this.viewHeight,this.scrollTop<0&&(this.scrollTop=0),this._emit("scroll",{data:this.scrollTop}))},this.setScrollTop=function(e){this.scrollTop=e,e<0&&(e=0),this.thumbTop=e*(this.slideHeight-this.thumbHeight)/(this.pageHeight-this.viewHeight),this.inner.style.top=this.thumbTop+"px"}}.call(a.prototype);var f=function(e,t){u.call(this,e),this.scrollLeft=0,this.scrollWidth=0,this.height=this.HScrollHeight,this.inner.style.height=this.element.style.height=(this.height||12)+"px",this.renderer=t};r.inherits(f,u),function(){this.classSuffix="-h",r.implement(this,o),this.onMouseDown=function(e,t){if(e!=="mousedown")return;if(s.getButton(t)!==0||t.detail===2)return;if(t.target===this.inner){var n=this,r=t.clientX,i=function(e){r=e.clientX},o=function(){clearInterval(l)},u=t.clientX,a=this.thumbLeft,f=function(){if(r===undefined)return;var e=n.scrollLeftFromThumbLeft(a+r-u);if(e===n.scrollLeft)return;n._emit("scroll",{data:e})};s.capture(this.inner,i,o);var l=setInterval(f,20);return s.preventDefault(t)}var c=t.clientX-this.element.getBoundingClientRect().left-this.thumbWidth/2;return this._emit("scroll",{data:this.scrollLeftFromThumbLeft(c)}),s.preventDefault(t)},this.getHeight=function(){return this.isVisible?this.height:0},this.scrollLeftFromThumbLeft=function(e){var t=e*(this.pageWidth-this.viewWidth)/(this.slideWidth-this.thumbWidth);return t>>=0,t<0?t=0:t>this.pageWidth-this.viewWidth&&(t=this.pageWidth-this.viewWidth),t},this.setWidth=function(e){this.width=Math.max(0,e),this.element.style.width=this.width+"px",this.slideWidth=this.width,this.viewWidth=this.width,this.setScrollWidth(this.pageWidth,!0)},this.setInnerWidth=this.setScrollWidth=function(e,t){if(this.pageWidth===e&&!t)return;this.pageWidth=e,this.thumbWidth=this.slideWidth*this.viewWidth/this.pageWidth,this.thumbWidth>this.slideWidth&&(this.thumbWidth=this.slideWidth),this.thumbWidth<15&&(this.thumbWidth=15),this.inner.style.width=this.thumbWidth+"px",this.scrollLeft>this.pageWidth-this.viewWidth&&(this.scrollLeft=this.pageWidth-this.viewWidth,this.scrollLeft<0&&(this.scrollLeft=0),this._emit("scroll",{data:this.scrollLeft}))},this.setScrollLeft=function(e){this.scrollLeft=e,e<0&&(e=0),this.thumbLeft=e*(this.slideWidth-this.thumbWidth)/(this.pageWidth-this.viewWidth),this.inner.style.left=this.thumbLeft+"px"}}.call(f.prototype),t.ScrollBar=a,t.ScrollBarV=a,t.ScrollBarH=f,t.VScrollBar=a,t.HScrollBar=f}),ace.define("ace/renderloop",["require","exports","module","ace/lib/event"],function(e,t,n){"use strict";var r=e("./lib/event"),i=function(e,t){this.onRender=e,this.pending=!1,this.changes=0,this.$recursionLimit=2,this.window=t||window;var n=this;this._flush=function(e){n.pending=!1;var t=n.changes;t&&(r.blockIdle(100),n.changes=0,n.onRender(t));if(n.changes){if(n.$recursionLimit--<0)return;n.schedule()}else n.$recursionLimit=2}};(function(){this.schedule=function(e){this.changes=this.changes|e,this.changes&&!this.pending&&(r.nextFrame(this._flush),this.pending=!0)},this.clear=function(e){var t=this.changes;return this.changes=0,t}}).call(i.prototype),t.RenderLoop=i}),ace.define("ace/layer/font_metrics",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/event","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/event"),u=e("../lib/useragent"),a=e("../lib/event_emitter").EventEmitter,f=250,l=typeof ResizeObserver=="function",c=200,h=t.FontMetrics=function(e,t){this.charCount=t||f,this.el=i.createElement("div"),this.$setMeasureNodeStyles(this.el.style,!0),this.$main=i.createElement("div"),this.$setMeasureNodeStyles(this.$main.style),this.$measureNode=i.createElement("div"),this.$setMeasureNodeStyles(this.$measureNode.style),this.el.appendChild(this.$main),this.el.appendChild(this.$measureNode),e.appendChild(this.el),this.$measureNode.textContent=s.stringRepeat("X",this.charCount),this.$characterSize={width:0,height:0},l?this.$addObserver():this.checkForSizeChanges()};(function(){r.implement(this,a),this.$characterSize={width:0,height:0},this.$setMeasureNodeStyles=function(e,t){e.width=e.height="auto",e.left=e.top="0px",e.visibility="hidden",e.position="absolute",e.whiteSpace="pre",u.isIE<8?e["font-family"]="inherit":e.font="inherit",e.overflow=t?"hidden":"visible"},this.checkForSizeChanges=function(e){e===undefined&&(e=this.$measureSizes());if(e&&(this.$characterSize.width!==e.width||this.$characterSize.height!==e.height)){this.$measureNode.style.fontWeight="bold";var t=this.$measureSizes();this.$measureNode.style.fontWeight="",this.$characterSize=e,this.charSizes=Object.create(null),this.allowBoldFonts=t&&t.width===e.width&&t.height===e.height,this._emit("changeCharacterSize",{data:e})}},this.$addObserver=function(){var e=this;this.$observer=new window.ResizeObserver(function(t){e.checkForSizeChanges()}),this.$observer.observe(this.$measureNode)},this.$pollSizeChanges=function(){if(this.$pollSizeChangesTimer||this.$observer)return this.$pollSizeChangesTimer;var e=this;return this.$pollSizeChangesTimer=o.onIdle(function t(){e.checkForSizeChanges(),o.onIdle(t,500)},500)},this.setPolling=function(e){e?this.$pollSizeChanges():this.$pollSizeChangesTimer&&(clearInterval(this.$pollSizeChangesTimer),this.$pollSizeChangesTimer=0)},this.$measureSizes=function(e){e=e||this.$measureNode;var t=e.getBoundingClientRect(),n={height:t.height,width:t.width/this.charCount};return n.width===0||n.height===0?null:n},this.$measureCharWidth=function(e){this.$main.textContent=s.stringRepeat(e,this.charCount);var t=this.$main.getBoundingClientRect();return t.width/this.charCount},this.getCharacterWidth=function(e){var t=this.charSizes[e];return t===undefined&&(t=this.charSizes[e]=this.$measureCharWidth(e)/this.$characterSize.width),t},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer),this.$observer&&this.$observer.disconnect(),this.el&&this.el.parentNode&&this.el.parentNode.removeChild(this.el)},this.$getZoom=function e(t){return!t||!t.parentElement?1:(window.getComputedStyle(t).zoom||1)*e(t.parentElement)},this.$initTransformMeasureNodes=function(){var e=function(e,t){return["div",{style:"position: absolute;top:"+e+"px;left:"+t+"px;"}]};this.els=i.buildDom([e(0,0),e(c,0),e(0,c),e(c,c)],this.el)},this.transformCoordinates=function(e,t){function r(e,t,n){var r=e[1]*t[0]-e[0]*t[1];return[(-t[1]*n[0]+t[0]*n[1])/r,(+e[1]*n[0]-e[0]*n[1])/r]}function i(e,t){return[e[0]-t[0],e[1]-t[1]]}function s(e,t){return[e[0]+t[0],e[1]+t[1]]}function o(e,t){return[e*t[0],e*t[1]]}function u(e){var t=e.getBoundingClientRect();return[t.left,t.top]}if(e){var n=this.$getZoom(this.el);e=o(1/n,e)}this.els||this.$initTransformMeasureNodes();var a=u(this.els[0]),f=u(this.els[1]),l=u(this.els[2]),h=u(this.els[3]),p=r(i(h,f),i(h,l),i(s(f,l),s(h,a))),d=o(1+p[0],i(f,a)),v=o(1+p[1],i(l,a));if(t){var m=t,g=p[0]*m[0]/c+p[1]*m[1]/c+1,y=s(o(m[0],d),o(m[1],v));return s(o(1/g/c,y),a)}var b=i(e,a),w=r(i(d,o(p[0],b)),i(v,o(p[1],b)),b);return o(c,w)}}).call(h.prototype)}),ace.define("ace/css/editor.css",["require","exports","module"],function(e,t,n){n.exports='/*\nstyles = []\nfor (var i = 1; i < 16; i++) {\n styles.push(".ace_br" + i + "{" + (\n ["top-left", "top-right", "bottom-right", "bottom-left"]\n ).map(function(x, j) {\n return i & (1< .ace_line, .ace_text-layer > .ace_line_group {\n contain: style size layout;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n}\n\n.ace_hidpi .ace_text-layer,\n.ace_hidpi .ace_gutter-layer,\n.ace_hidpi .ace_content,\n.ace_hidpi .ace_gutter {\n contain: strict;\n will-change: transform;\n}\n.ace_hidpi .ace_text-layer > .ace_line, \n.ace_hidpi .ace_text-layer > .ace_line_group {\n contain: strict;\n}\n\n.ace_cjk {\n display: inline-block;\n text-align: center;\n}\n\n.ace_cursor-layer {\n z-index: 4;\n}\n\n.ace_cursor {\n z-index: 4;\n position: absolute;\n box-sizing: border-box;\n border-left: 2px solid;\n /* workaround for smooth cursor repaintng whole screen in chrome */\n transform: translatez(0);\n}\n\n.ace_multiselect .ace_cursor {\n border-left-width: 1px;\n}\n\n.ace_slim-cursors .ace_cursor {\n border-left-width: 1px;\n}\n\n.ace_overwrite-cursors .ace_cursor {\n border-left-width: 0;\n border-bottom: 1px solid;\n}\n\n.ace_hidden-cursors .ace_cursor {\n opacity: 0.2;\n}\n\n.ace_hasPlaceholder .ace_hidden-cursors .ace_cursor {\n opacity: 0;\n}\n\n.ace_smooth-blinking .ace_cursor {\n transition: opacity 0.18s;\n}\n\n.ace_animate-blinking .ace_cursor {\n animation-duration: 1000ms;\n animation-timing-function: step-end;\n animation-name: blink-ace-animate;\n animation-iteration-count: infinite;\n}\n\n.ace_animate-blinking.ace_smooth-blinking .ace_cursor {\n animation-duration: 1000ms;\n animation-timing-function: ease-in-out;\n animation-name: blink-ace-animate-smooth;\n}\n \n@keyframes blink-ace-animate {\n from, to { opacity: 1; }\n 60% { opacity: 0; }\n}\n\n@keyframes blink-ace-animate-smooth {\n from, to { opacity: 1; }\n 45% { opacity: 1; }\n 60% { opacity: 0; }\n 85% { opacity: 0; }\n}\n\n.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {\n position: absolute;\n z-index: 3;\n}\n\n.ace_marker-layer .ace_selection {\n position: absolute;\n z-index: 5;\n}\n\n.ace_marker-layer .ace_bracket {\n position: absolute;\n z-index: 6;\n}\n\n.ace_marker-layer .ace_error_bracket {\n position: absolute;\n border-bottom: 1px solid #DE5555;\n border-radius: 0;\n}\n\n.ace_marker-layer .ace_active-line {\n position: absolute;\n z-index: 2;\n}\n\n.ace_marker-layer .ace_selected-word {\n position: absolute;\n z-index: 4;\n box-sizing: border-box;\n}\n\n.ace_line .ace_fold {\n box-sizing: border-box;\n\n display: inline-block;\n height: 11px;\n margin-top: -2px;\n vertical-align: middle;\n\n background-image:\n url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),\n url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=");\n background-repeat: no-repeat, repeat-x;\n background-position: center center, top left;\n color: transparent;\n\n border: 1px solid black;\n border-radius: 2px;\n\n cursor: pointer;\n pointer-events: auto;\n}\n\n.ace_dark .ace_fold {\n}\n\n.ace_fold:hover{\n background-image:\n url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),\n url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC");\n}\n\n.ace_tooltip {\n background-color: #FFF;\n background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));\n border: 1px solid gray;\n border-radius: 1px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n color: black;\n max-width: 100%;\n padding: 3px 4px;\n position: fixed;\n z-index: 999999;\n box-sizing: border-box;\n cursor: default;\n white-space: pre;\n word-wrap: break-word;\n line-height: normal;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n pointer-events: none;\n}\n\n.ace_folding-enabled > .ace_gutter-cell {\n padding-right: 13px;\n}\n\n.ace_fold-widget {\n box-sizing: border-box;\n\n margin: 0 -12px 0 1px;\n display: none;\n width: 11px;\n vertical-align: top;\n\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");\n background-repeat: no-repeat;\n background-position: center;\n\n border-radius: 3px;\n \n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.ace_folding-enabled .ace_fold-widget {\n display: inline-block; \n}\n\n.ace_fold-widget.ace_end {\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==");\n}\n\n.ace_fold-widget.ace_closed {\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");\n}\n\n.ace_fold-widget:hover {\n border: 1px solid rgba(0, 0, 0, 0.3);\n background-color: rgba(255, 255, 255, 0.2);\n box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);\n}\n\n.ace_fold-widget:active {\n border: 1px solid rgba(0, 0, 0, 0.4);\n background-color: rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);\n}\n/**\n * Dark version for fold widgets\n */\n.ace_dark .ace_fold-widget {\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC");\n}\n.ace_dark .ace_fold-widget.ace_end {\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==");\n}\n.ace_dark .ace_fold-widget.ace_closed {\n background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==");\n}\n.ace_dark .ace_fold-widget:hover {\n box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);\n background-color: rgba(255, 255, 255, 0.1);\n}\n.ace_dark .ace_fold-widget:active {\n box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);\n}\n\n.ace_inline_button {\n border: 1px solid lightgray;\n display: inline-block;\n margin: -1px 8px;\n padding: 0 5px;\n pointer-events: auto;\n cursor: pointer;\n}\n.ace_inline_button:hover {\n border-color: gray;\n background: rgba(200,200,200,0.2);\n display: inline-block;\n pointer-events: auto;\n}\n\n.ace_fold-widget.ace_invalid {\n background-color: #FFB4B4;\n border-color: #DE5555;\n}\n\n.ace_fade-fold-widgets .ace_fold-widget {\n transition: opacity 0.4s ease 0.05s;\n opacity: 0;\n}\n\n.ace_fade-fold-widgets:hover .ace_fold-widget {\n transition: opacity 0.05s ease 0.05s;\n opacity:1;\n}\n\n.ace_underline {\n text-decoration: underline;\n}\n\n.ace_bold {\n font-weight: bold;\n}\n\n.ace_nobold .ace_bold {\n font-weight: normal;\n}\n\n.ace_italic {\n font-style: italic;\n}\n\n\n.ace_error-marker {\n background-color: rgba(255, 0, 0,0.2);\n position: absolute;\n z-index: 9;\n}\n\n.ace_highlight-marker {\n background-color: rgba(255, 255, 0,0.2);\n position: absolute;\n z-index: 8;\n}\n\n.ace_mobile-menu {\n position: absolute;\n line-height: 1.5;\n border-radius: 4px;\n -ms-user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n user-select: none;\n background: white;\n box-shadow: 1px 3px 2px grey;\n border: 1px solid #dcdcdc;\n color: black;\n}\n.ace_dark > .ace_mobile-menu {\n background: #333;\n color: #ccc;\n box-shadow: 1px 3px 2px grey;\n border: 1px solid #444;\n\n}\n.ace_mobile-button {\n padding: 2px;\n cursor: pointer;\n overflow: hidden;\n}\n.ace_mobile-button:hover {\n background-color: #eee;\n opacity:1;\n}\n.ace_mobile-button:active {\n background-color: #ddd;\n}\n\n.ace_placeholder {\n font-family: arial;\n transform: scale(0.9);\n transform-origin: left;\n white-space: pre;\n opacity: 0.7;\n margin: 0 10px;\n}\n\n.ace_ghost_text {\n opacity: 0.5;\n font-style: italic;\n}'}),ace.define("ace/layer/decorators",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event_emitter").EventEmitter,o=function(e,t){this.canvas=r.createElement("canvas"),this.renderer=t,this.pixelRatio=1,this.maxHeight=t.layerConfig.maxHeight,this.lineHeight=t.layerConfig.lineHeight,this.canvasHeight=e.parent.scrollHeight,this.heightRatio=this.canvasHeight/this.maxHeight,this.canvasWidth=e.width,this.minDecorationHeight=2*this.pixelRatio|0,this.halfMinDecorationHeight=this.minDecorationHeight/2|0,this.canvas.width=this.canvasWidth,this.canvas.height=this.canvasHeight,this.canvas.style.top="0px",this.canvas.style.right="0px",this.canvas.style.zIndex="7px",this.canvas.style.position="absolute",this.colors={},this.colors.dark={error:"rgba(255, 18, 18, 1)",warning:"rgba(18, 136, 18, 1)",info:"rgba(18, 18, 136, 1)"},this.colors.light={error:"rgb(255,51,51)",warning:"rgb(32,133,72)",info:"rgb(35,68,138)"},e.element.appendChild(this.canvas)};(function(){i.implement(this,s),this.$updateDecorators=function(e){function i(e,t){return e.priorityt.priority?1:0}var t=this.renderer.theme.isDark===!0?this.colors.dark:this.colors.light;if(e){this.maxHeight=e.maxHeight,this.lineHeight=e.lineHeight,this.canvasHeight=e.height;var n=(e.lastRow+1)*this.lineHeight;nthis.canvasHeight&&(v=this.canvasHeight-this.halfMinDecorationHeight),h=Math.round(v-this.halfMinDecorationHeight),p=Math.round(v+this.halfMinDecorationHeight)}r.fillStyle=t[s[a].type]||null,r.fillRect(0,c,this.canvasWidth,p-h)}}var m=this.renderer.session.selection.getCursor();if(m){var l=this.compensateFoldRows(m.row,u),c=Math.round((m.row-l)*this.lineHeight*this.heightRatio);r.fillStyle="rgba(0, 0, 0, 0.5)",r.fillRect(0,c,this.canvasWidth,2)}},this.compensateFoldRows=function(e,t){var n=0;if(t&&t.length>0)for(var r=0;rt[r].start.row&&e=t[r].end.row&&(n+=t[r].end.row-t[r].start.row);return n}}).call(o.prototype),t.Decorator=o}),ace.define("ace/virtual_renderer",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/config","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/scrollbar","ace/scrollbar_custom","ace/scrollbar_custom","ace/renderloop","ace/layer/font_metrics","ace/lib/event_emitter","ace/css/editor.css","ace/layer/decorators","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./config"),o=e("./layer/gutter").Gutter,u=e("./layer/marker").Marker,a=e("./layer/text").Text,f=e("./layer/cursor").Cursor,l=e("./scrollbar").HScrollBar,c=e("./scrollbar").VScrollBar,h=e("./scrollbar_custom").HScrollBar,p=e("./scrollbar_custom").VScrollBar,d=e("./renderloop").RenderLoop,v=e("./layer/font_metrics").FontMetrics,m=e("./lib/event_emitter").EventEmitter,g=e("./css/editor.css"),y=e("./layer/decorators").Decorator,b=e("./lib/useragent"),w=b.isIE;i.importCssString(g,"ace_editor.css",!1);var E=function(e,t){var n=this;this.container=e||i.createElement("div"),i.addCssClass(this.container,"ace_editor"),i.HI_DPI&&i.addCssClass(this.container,"ace_hidpi"),this.setTheme(t),s.get("useStrictCSP")==null&&s.set("useStrictCSP",!1),this.$gutter=i.createElement("div"),this.$gutter.className="ace_gutter",this.container.appendChild(this.$gutter),this.$gutter.setAttribute("aria-hidden",!0),this.scroller=i.createElement("div"),this.scroller.className="ace_scroller",this.container.appendChild(this.scroller),this.content=i.createElement("div"),this.content.className="ace_content",this.scroller.appendChild(this.content),this.$gutterLayer=new o(this.$gutter),this.$gutterLayer.on("changeGutterWidth",this.onGutterResize.bind(this)),this.$markerBack=new u(this.content);var r=this.$textLayer=new a(this.content);this.canvas=r.element,this.$markerFront=new u(this.content),this.$cursorLayer=new f(this.content),this.$horizScroll=!1,this.$vScroll=!1,this.scrollBar=this.scrollBarV=new c(this.container,this),this.scrollBarH=new l(this.container,this),this.scrollBarV.on("scroll",function(e){n.$scrollAnimation||n.session.setScrollTop(e.data-n.scrollMargin.top)}),this.scrollBarH.on("scroll",function(e){n.$scrollAnimation||n.session.setScrollLeft(e.data-n.scrollMargin.left)}),this.scrollTop=0,this.scrollLeft=0,this.cursorPos={row:0,column:0},this.$fontMetrics=new v(this.container,this.$textLayer.MAX_CHUNK_LENGTH),this.$textLayer.$setFontMetrics(this.$fontMetrics),this.$textLayer.on("changeCharacterSize",function(e){n.updateCharacterSize(),n.onResize(!0,n.gutterWidth,n.$size.width,n.$size.height),n._signal("changeCharacterSize",e)}),this.$size={width:0,height:0,scrollerHeight:0,scrollerWidth:0,$dirty:!0},this.layerConfig={width:1,padding:0,firstRow:0,firstRowScreen:0,lastRow:0,lineHeight:0,characterWidth:0,minHeight:1,maxHeight:1,offset:0,height:1,gutterOffset:1},this.scrollMargin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.margin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.$keepTextAreaAtCursor=!b.isIOS,this.$loop=new d(this.$renderChanges.bind(this),this.container.ownerDocument.defaultView),this.$loop.schedule(this.CHANGE_FULL),this.updateCharacterSize(),this.setPadding(4),s.resetOptions(this),s._signal("renderer",this)};(function(){this.CHANGE_CURSOR=1,this.CHANGE_MARKER=2,this.CHANGE_GUTTER=4,this.CHANGE_SCROLL=8,this.CHANGE_LINES=16,this.CHANGE_TEXT=32,this.CHANGE_SIZE=64,this.CHANGE_MARKER_BACK=128,this.CHANGE_MARKER_FRONT=256,this.CHANGE_FULL=512,this.CHANGE_H_SCROLL=1024,r.implement(this,m),this.updateCharacterSize=function(){this.$textLayer.allowBoldFonts!=this.$allowBoldFonts&&(this.$allowBoldFonts=this.$textLayer.allowBoldFonts,this.setStyle("ace_nobold",!this.$allowBoldFonts)),this.layerConfig.characterWidth=this.characterWidth=this.$textLayer.getCharacterWidth(),this.layerConfig.lineHeight=this.lineHeight=this.$textLayer.getLineHeight(),this.$updatePrintMargin(),i.setStyle(this.scroller.style,"line-height",this.lineHeight+"px")},this.setSession=function(e){this.session&&this.session.doc.off("changeNewLineMode",this.onChangeNewLineMode),this.session=e,e&&this.scrollMargin.top&&e.getScrollTop()<=0&&e.setScrollTop(-this.scrollMargin.top),this.$cursorLayer.setSession(e),this.$markerBack.setSession(e),this.$markerFront.setSession(e),this.$gutterLayer.setSession(e),this.$textLayer.setSession(e);if(!e)return;this.$loop.schedule(this.CHANGE_FULL),this.session.$setFontMetrics(this.$fontMetrics),this.scrollBarH.scrollLeft=this.scrollBarV.scrollTop=null,this.onChangeNewLineMode=this.onChangeNewLineMode.bind(this),this.onChangeNewLineMode(),this.session.doc.on("changeNewLineMode",this.onChangeNewLineMode)},this.updateLines=function(e,t,n){t===undefined&&(t=Infinity),this.$changedLines?(this.$changedLines.firstRow>e&&(this.$changedLines.firstRow=e),this.$changedLines.lastRowthis.layerConfig.lastRow)return;this.$loop.schedule(this.CHANGE_LINES)},this.onChangeNewLineMode=function(){this.$loop.schedule(this.CHANGE_TEXT),this.$textLayer.$updateEolChar(),this.session.$bidiHandler.setEolChar(this.$textLayer.EOL_CHAR)},this.onChangeTabSize=function(){this.$loop.schedule(this.CHANGE_TEXT|this.CHANGE_MARKER),this.$textLayer.onChangeTabSize()},this.updateText=function(){this.$loop.schedule(this.CHANGE_TEXT)},this.updateFull=function(e){e?this.$renderChanges(this.CHANGE_FULL,!0):this.$loop.schedule(this.CHANGE_FULL)},this.updateFontSize=function(){this.$textLayer.checkForSizeChanges()},this.$changes=0,this.$updateSizeAsync=function(){this.$loop.pending?this.$size.$dirty=!0:this.onResize()},this.onResize=function(e,t,n,r){if(this.resizing>2)return;this.resizing>0?this.resizing++:this.resizing=e?1:0;var i=this.container;r||(r=i.clientHeight||i.scrollHeight),n||(n=i.clientWidth||i.scrollWidth);var s=this.$updateCachedSize(e,t,n,r);if(!this.$size.scrollerHeight||!n&&!r)return this.resizing=0;e&&(this.$gutterLayer.$padding=null),e?this.$renderChanges(s|this.$changes,!0):this.$loop.schedule(s|this.$changes),this.resizing&&(this.resizing=0),this.scrollBarH.scrollLeft=this.scrollBarV.scrollTop=null,this.$customScrollbar&&this.$updateCustomScrollbar(!0)},this.$updateCachedSize=function(e,t,n,r){r-=this.$extraHeight||0;var s=0,o=this.$size,u={width:o.width,height:o.height,scrollerHeight:o.scrollerHeight,scrollerWidth:o.scrollerWidth};r&&(e||o.height!=r)&&(o.height=r,s|=this.CHANGE_SIZE,o.scrollerHeight=o.height,this.$horizScroll&&(o.scrollerHeight-=this.scrollBarH.getHeight()),this.scrollBarV.setHeight(o.scrollerHeight),this.scrollBarV.element.style.bottom=this.scrollBarH.getHeight()+"px",s|=this.CHANGE_SCROLL);if(n&&(e||o.width!=n)){s|=this.CHANGE_SIZE,o.width=n,t==null&&(t=this.$showGutter?this.$gutter.offsetWidth:0),this.gutterWidth=t,i.setStyle(this.scrollBarH.element.style,"left",t+"px"),i.setStyle(this.scroller.style,"left",t+this.margin.left+"px"),o.scrollerWidth=Math.max(0,n-t-this.scrollBarV.getWidth()-this.margin.h),i.setStyle(this.$gutter.style,"left",this.margin.left+"px");var a=this.scrollBarV.getWidth()+"px";i.setStyle(this.scrollBarH.element.style,"right",a),i.setStyle(this.scroller.style,"right",a),i.setStyle(this.scroller.style,"bottom",this.scrollBarH.getHeight()),this.scrollBarH.setWidth(o.scrollerWidth);if(this.session&&this.session.getUseWrapMode()&&this.adjustWrapLimit()||e)s|=this.CHANGE_FULL}return o.$dirty=!n||!r,s&&this._signal("resize",u),s},this.onGutterResize=function(e){var t=this.$showGutter?e:0;t!=this.gutterWidth&&(this.$changes|=this.$updateCachedSize(!0,t,this.$size.width,this.$size.height)),this.session.getUseWrapMode()&&this.adjustWrapLimit()?this.$loop.schedule(this.CHANGE_FULL):this.$size.$dirty?this.$loop.schedule(this.CHANGE_FULL):this.$computeLayerConfig()},this.adjustWrapLimit=function(){var e=this.$size.scrollerWidth-this.$padding*2,t=Math.floor(e/this.characterWidth);return this.session.adjustWrapLimit(t,this.$showPrintMargin&&this.$printMarginColumn)},this.setAnimatedScroll=function(e){this.setOption("animatedScroll",e)},this.getAnimatedScroll=function(){return this.$animatedScroll},this.setShowInvisibles=function(e){this.setOption("showInvisibles",e),this.session.$bidiHandler.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.getOption("showInvisibles")},this.getDisplayIndentGuides=function(){return this.getOption("displayIndentGuides")},this.setDisplayIndentGuides=function(e){this.setOption("displayIndentGuides",e)},this.getHighlightIndentGuides=function(){return this.getOption("highlightIndentGuides")},this.setHighlightIndentGuides=function(e){this.setOption("highlightIndentGuides",e)},this.setShowPrintMargin=function(e){this.setOption("showPrintMargin",e)},this.getShowPrintMargin=function(){return this.getOption("showPrintMargin")},this.setPrintMarginColumn=function(e){this.setOption("printMarginColumn",e)},this.getPrintMarginColumn=function(){return this.getOption("printMarginColumn")},this.getShowGutter=function(){return this.getOption("showGutter")},this.setShowGutter=function(e){return this.setOption("showGutter",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.$updatePrintMargin=function(){if(!this.$showPrintMargin&&!this.$printMarginEl)return;if(!this.$printMarginEl){var e=i.createElement("div");e.className="ace_layer ace_print-margin-layer",this.$printMarginEl=i.createElement("div"),this.$printMarginEl.className="ace_print-margin",e.appendChild(this.$printMarginEl),this.content.insertBefore(e,this.content.firstChild)}var t=this.$printMarginEl.style;t.left=Math.round(this.characterWidth*this.$printMarginColumn+this.$padding)+"px",t.visibility=this.$showPrintMargin?"visible":"hidden",this.session&&this.session.$wrap==-1&&this.adjustWrapLimit()},this.getContainerElement=function(){return this.container},this.getMouseEventTarget=function(){return this.scroller},this.getTextAreaContainer=function(){return this.container},this.$moveTextAreaToCursor=function(){if(this.$isMousePressed)return;var e=this.textarea.style,t=this.$composition;if(!this.$keepTextAreaAtCursor&&!t){i.translate(this.textarea,-100,0);return}var n=this.$cursorLayer.$pixelPos;if(!n)return;t&&t.markerRange&&(n=this.$cursorLayer.getPixelPosition(t.markerRange.start,!0));var r=this.layerConfig,s=n.top,o=n.left;s-=r.offset;var u=t&&t.useTextareaForIME?this.lineHeight:w?0:1;if(s<0||s>r.height-u){i.translate(this.textarea,0,0);return}var a=1,f=this.$size.height-u;if(!t)s+=this.lineHeight;else if(t.useTextareaForIME){var l=this.textarea.value;a=this.characterWidth*this.session.$getStringScreenWidth(l)[0]}else s+=this.lineHeight+2;o-=this.scrollLeft,o>this.$size.scrollerWidth-a&&(o=this.$size.scrollerWidth-a),o+=this.gutterWidth+this.margin.left,i.setStyle(e,"height",u+"px"),i.setStyle(e,"width",a+"px"),i.translate(this.textarea,Math.min(o,this.$size.scrollerWidth-a),Math.min(s,f))},this.getFirstVisibleRow=function(){return this.layerConfig.firstRow},this.getFirstFullyVisibleRow=function(){return this.layerConfig.firstRow+(this.layerConfig.offset===0?0:1)},this.getLastFullyVisibleRow=function(){var e=this.layerConfig,t=e.lastRow,n=this.session.documentToScreenRow(t,0)*e.lineHeight;return n-this.session.getScrollTop()>e.height-e.lineHeight?t-1:t},this.getLastVisibleRow=function(){return this.layerConfig.lastRow},this.$padding=null,this.setPadding=function(e){this.$padding=e,this.$textLayer.setPadding(e),this.$cursorLayer.setPadding(e),this.$markerFront.setPadding(e),this.$markerBack.setPadding(e),this.$loop.schedule(this.CHANGE_FULL),this.$updatePrintMargin()},this.setScrollMargin=function(e,t,n,r){var i=this.scrollMargin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,i.top&&this.scrollTop<=0&&this.session&&this.session.setScrollTop(-i.top),this.updateFull()},this.setMargin=function(e,t,n,r){var i=this.margin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,this.$updateCachedSize(!0,this.gutterWidth,this.$size.width,this.$size.height),this.updateFull()},this.getHScrollBarAlwaysVisible=function(){return this.$hScrollBarAlwaysVisible},this.setHScrollBarAlwaysVisible=function(e){this.setOption("hScrollBarAlwaysVisible",e)},this.getVScrollBarAlwaysVisible=function(){return this.$vScrollBarAlwaysVisible},this.setVScrollBarAlwaysVisible=function(e){this.setOption("vScrollBarAlwaysVisible",e)},this.$updateScrollBarV=function(){var e=this.layerConfig.maxHeight,t=this.$size.scrollerHeight;!this.$maxLines&&this.$scrollPastEnd&&(e-=(t-this.lineHeight)*this.$scrollPastEnd,this.scrollTop>e-t&&(e=this.scrollTop+t,this.scrollBarV.scrollTop=null)),this.scrollBarV.setScrollHeight(e+this.scrollMargin.v),this.scrollBarV.setScrollTop(this.scrollTop+this.scrollMargin.top)},this.$updateScrollBarH=function(){this.scrollBarH.setScrollWidth(this.layerConfig.width+2*this.$padding+this.scrollMargin.h),this.scrollBarH.setScrollLeft(this.scrollLeft+this.scrollMargin.left)},this.$frozen=!1,this.freeze=function(){this.$frozen=!0},this.unfreeze=function(){this.$frozen=!1},this.$renderChanges=function(e,t){this.$changes&&(e|=this.$changes,this.$changes=0);if(!this.session||!this.container.offsetWidth||this.$frozen||!e&&!t){this.$changes|=e;return}if(this.$size.$dirty)return this.$changes|=e,this.onResize(!0);this.lineHeight||this.$textLayer.checkForSizeChanges(),this._signal("beforeRender",e),this.session&&this.session.$bidiHandler&&this.session.$bidiHandler.updateCharacterWidths(this.$fontMetrics);var n=this.layerConfig;if(e&this.CHANGE_FULL||e&this.CHANGE_SIZE||e&this.CHANGE_TEXT||e&this.CHANGE_LINES||e&this.CHANGE_SCROLL||e&this.CHANGE_H_SCROLL){e|=this.$computeLayerConfig()|this.$loop.clear();if(n.firstRow!=this.layerConfig.firstRow&&n.firstRowScreen==this.layerConfig.firstRowScreen){var r=this.scrollTop+(n.firstRow-this.layerConfig.firstRow)*this.lineHeight;r>0&&(this.scrollTop=r,e|=this.CHANGE_SCROLL,e|=this.$computeLayerConfig()|this.$loop.clear())}n=this.layerConfig,this.$updateScrollBarV(),e&this.CHANGE_H_SCROLL&&this.$updateScrollBarH(),i.translate(this.content,-this.scrollLeft,-n.offset);var s=n.width+2*this.$padding+"px",o=n.minHeight+"px";i.setStyle(this.content.style,"width",s),i.setStyle(this.content.style,"height",o)}e&this.CHANGE_H_SCROLL&&(i.translate(this.content,-this.scrollLeft,-n.offset),this.scroller.className=this.scrollLeft<=0?"ace_scroller":"ace_scroller ace_scroll-left");if(e&this.CHANGE_FULL){this.$changedLines=null,this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender",e);return}if(e&this.CHANGE_SCROLL){this.$changedLines=null,e&this.CHANGE_TEXT||e&this.CHANGE_LINES?this.$textLayer.update(n):this.$textLayer.scrollLines(n),this.$showGutter&&(e&this.CHANGE_GUTTER||e&this.CHANGE_LINES?this.$gutterLayer.update(n):this.$gutterLayer.scrollLines(n)),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender",e);return}e&this.CHANGE_TEXT?(this.$changedLines=null,this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n)):e&this.CHANGE_LINES?((this.$updateLines()||e&this.CHANGE_GUTTER&&this.$showGutter)&&this.$gutterLayer.update(n),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n)):e&this.CHANGE_TEXT||e&this.CHANGE_GUTTER?(this.$showGutter&&this.$gutterLayer.update(n),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n)):e&this.CHANGE_CURSOR&&(this.$highlightGutterLine&&this.$gutterLayer.updateLineHighlight(n),this.$customScrollbar&&this.$scrollDecorator.$updateDecorators(n)),e&this.CHANGE_CURSOR&&(this.$cursorLayer.update(n),this.$moveTextAreaToCursor()),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_FRONT)&&this.$markerFront.update(n),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_BACK)&&this.$markerBack.update(n),this._signal("afterRender",e)},this.$autosize=function(){var e=this.session.getScreenLength()*this.lineHeight,t=this.$maxLines*this.lineHeight,n=Math.min(t,Math.max((this.$minLines||1)*this.lineHeight,e))+this.scrollMargin.v+(this.$extraHeight||0);this.$horizScroll&&(n+=this.scrollBarH.getHeight()),this.$maxPixelHeight&&n>this.$maxPixelHeight&&(n=this.$maxPixelHeight);var r=n<=2*this.lineHeight,i=!r&&e>t;if(n!=this.desiredHeight||this.$size.height!=this.desiredHeight||i!=this.$vScroll){i!=this.$vScroll&&(this.$vScroll=i,this.scrollBarV.setVisible(i));var s=this.container.clientWidth;this.container.style.height=n+"px",this.$updateCachedSize(!0,this.$gutterWidth,s,n),this.desiredHeight=n,this._signal("autosize")}},this.$computeLayerConfig=function(){var e=this.session,t=this.$size,n=t.height<=2*this.lineHeight,r=this.session.getScreenLength(),i=r*this.lineHeight,s=this.$getLongestLine(),o=!n&&(this.$hScrollBarAlwaysVisible||t.scrollerWidth-s-2*this.$padding<0),u=this.$horizScroll!==o;u&&(this.$horizScroll=o,this.scrollBarH.setVisible(o));var a=this.$vScroll;this.$maxLines&&this.lineHeight>1&&this.$autosize();var f=t.scrollerHeight+this.lineHeight,l=!this.$maxLines&&this.$scrollPastEnd?(t.scrollerHeight-this.lineHeight)*this.$scrollPastEnd:0;i+=l;var c=this.scrollMargin;this.session.setScrollTop(Math.max(-c.top,Math.min(this.scrollTop,i-t.scrollerHeight+c.bottom))),this.session.setScrollLeft(Math.max(-c.left,Math.min(this.scrollLeft,s+2*this.$padding-t.scrollerWidth+c.right)));var h=!n&&(this.$vScrollBarAlwaysVisible||t.scrollerHeight-i+l<0||this.scrollTop>c.top),p=a!==h;p&&(this.$vScroll=h,this.scrollBarV.setVisible(h));var d=this.scrollTop%this.lineHeight,v=Math.ceil(f/this.lineHeight)-1,m=Math.max(0,Math.round((this.scrollTop-d)/this.lineHeight)),g=m+v,y,b,w=this.lineHeight;m=e.screenToDocumentRow(m,0);var E=e.getFoldLine(m);E&&(m=E.start.row),y=e.documentToScreenRow(m,0),b=e.getRowLength(m)*w,g=Math.min(e.screenToDocumentRow(g,0),e.getLength()-1),f=t.scrollerHeight+e.getRowLength(g)*w+b,d=this.scrollTop-y*w;var S=0;if(this.layerConfig.width!=s||u)S=this.CHANGE_H_SCROLL;if(u||p)S|=this.$updateCachedSize(!0,this.gutterWidth,t.width,t.height),this._signal("scrollbarVisibilityChanged"),p&&(s=this.$getLongestLine());return this.layerConfig={width:s,padding:this.$padding,firstRow:m,firstRowScreen:y,lastRow:g,lineHeight:w,characterWidth:this.characterWidth,minHeight:f,maxHeight:i,offset:d,gutterOffset:w?Math.max(0,Math.ceil((d+t.height-t.scrollerHeight)/w)):0,height:this.$size.scrollerHeight},this.session.$bidiHandler&&this.session.$bidiHandler.setContentWidth(s-this.$padding),S},this.$updateLines=function(){if(!this.$changedLines)return;var e=this.$changedLines.firstRow,t=this.$changedLines.lastRow;this.$changedLines=null;var n=this.layerConfig;if(e>n.lastRow+1)return;if(tthis.$textLayer.MAX_LINE_LENGTH&&(e=this.$textLayer.MAX_LINE_LENGTH+30),Math.max(this.$size.scrollerWidth-2*this.$padding,Math.round(e*this.characterWidth))},this.updateFrontMarkers=function(){this.$markerFront.setMarkers(this.session.getMarkers(!0)),this.$loop.schedule(this.CHANGE_MARKER_FRONT)},this.updateBackMarkers=function(){this.$markerBack.setMarkers(this.session.getMarkers()),this.$loop.schedule(this.CHANGE_MARKER_BACK)},this.addGutterDecoration=function(e,t){this.$gutterLayer.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){this.$gutterLayer.removeGutterDecoration(e,t)},this.updateBreakpoints=function(e){this.$loop.schedule(this.CHANGE_GUTTER)},this.setAnnotations=function(e){this.$gutterLayer.setAnnotations(e),this.$loop.schedule(this.CHANGE_GUTTER)},this.updateCursor=function(){this.$loop.schedule(this.CHANGE_CURSOR)},this.hideCursor=function(){this.$cursorLayer.hideCursor()},this.showCursor=function(){this.$cursorLayer.showCursor()},this.scrollSelectionIntoView=function(e,t,n){this.scrollCursorIntoView(e,n),this.scrollCursorIntoView(t,n)},this.scrollCursorIntoView=function(e,t,n){if(this.$size.scrollerHeight===0)return;var r=this.$cursorLayer.getPixelPosition(e),i=r.left,s=r.top,o=n&&n.top||0,u=n&&n.bottom||0;this.$scrollAnimation&&(this.$stopAnimation=!0);var a=this.$scrollAnimation?this.session.getScrollTop():this.scrollTop;a+o>s?(t&&a+o>s+this.lineHeight&&(s-=t*this.$size.scrollerHeight),s===0&&(s=-this.scrollMargin.top),this.session.setScrollTop(s)):a+this.$size.scrollerHeight-u=1-this.scrollMargin.top)return!0;if(t>0&&this.session.getScrollTop()+this.$size.scrollerHeight-this.layerConfig.maxHeight<-1+this.scrollMargin.bottom)return!0;if(e<0&&this.session.getScrollLeft()>=1-this.scrollMargin.left)return!0;if(e>0&&this.session.getScrollLeft()+this.$size.scrollerWidth-this.layerConfig.width<-1+this.scrollMargin.right)return!0},this.pixelToScreenCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=Math.floor((t+this.scrollTop-n.top)/this.lineHeight),u=this.$blockCursor?Math.floor(s):Math.round(s);return{row:o,column:u,side:s-u>0?1:-1,offsetX:i}},this.screenToTextCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=this.$blockCursor?Math.floor(s):Math.round(s),u=Math.floor((t+this.scrollTop-n.top)/this.lineHeight);return this.session.screenToDocumentPosition(u,Math.max(o,0),i)},this.textToScreenCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=this.session.documentToScreenPosition(e,t),i=this.$padding+(this.session.$bidiHandler.isBidiRow(r.row,e)?this.session.$bidiHandler.getPosLeft(r.column):Math.round(r.column*this.characterWidth)),s=r.row*this.lineHeight;return{pageX:n.left+i-this.scrollLeft,pageY:n.top+s-this.scrollTop}},this.visualizeFocus=function(){i.addCssClass(this.container,"ace_focus")},this.visualizeBlur=function(){i.removeCssClass(this.container,"ace_focus")},this.showComposition=function(e){this.$composition=e,e.cssText||(e.cssText=this.textarea.style.cssText),e.useTextareaForIME==undefined&&(e.useTextareaForIME=this.$useTextareaForIME),this.$useTextareaForIME?(i.addCssClass(this.textarea,"ace_composition"),this.textarea.style.cssText="",this.$moveTextAreaToCursor(),this.$cursorLayer.element.style.display="none"):e.markerId=this.session.addMarker(e.markerRange,"ace_composition_marker","text")},this.setCompositionText=function(e){var t=this.session.selection.cursor;this.addToken(e,"composition_placeholder",t.row,t.column),this.$moveTextAreaToCursor()},this.hideComposition=function(){if(!this.$composition)return;this.$composition.markerId&&this.session.removeMarker(this.$composition.markerId),i.removeCssClass(this.textarea,"ace_composition"),this.textarea.style.cssText=this.$composition.cssText;var e=this.session.selection.cursor;this.removeExtraToken(e.row,e.column),this.$composition=null,this.$cursorLayer.element.style.display=""},this.setGhostText=function(e,t){var n=this.session.selection.cursor,r=t||{row:n.row,column:n.column};this.removeGhostText();var i=e.split("\n");this.addToken(i[0],"ghost_text",r.row,r.column),this.$ghostText={text:e,position:{row:r.row,column:r.column}},i.length>1&&(this.$ghostTextWidget={text:i.slice(1).join("\n"),row:r.row,column:r.column,className:"ace_ghost_text"},this.session.widgetManager.addLineWidget(this.$ghostTextWidget))},this.removeGhostText=function(){if(!this.$ghostText)return;var e=this.$ghostText.position;this.removeExtraToken(e.row,e.column),this.$ghostTextWidget&&(this.session.widgetManager.removeLineWidget(this.$ghostTextWidget),this.$ghostTextWidget=null),this.$ghostText=null},this.addToken=function(e,t,n,r){var i=this.session;i.bgTokenizer.lines[n]=null;var s={type:t,value:e},o=i.getTokens(n);if(r==null)o.push(s);else{var u=0;for(var a=0;a50&&e.length>this.$doc.getLength()>>1?this.call("setValue",[this.$doc.getValue()]):this.emit("change",{data:e})}}).call(f.prototype);var l=function(e,t,n){var r=null,i=!1,u=Object.create(s),a=[],l=new f({messageBuffer:a,terminate:function(){},postMessage:function(e){a.push(e);if(!r)return;i?setTimeout(c):c()}});l.setEmitSync=function(e){i=e};var c=function(){var e=a.shift();e.command?r[e.command].apply(r,e.args):e.event&&u._signal(e.event,e.data)};return u.postMessage=function(e){l.onMessage({data:e})},u.callback=function(e,t){this.postMessage({type:"call",id:t,data:e})},u.emit=function(e,t){this.postMessage({type:"event",name:e,data:t})},o.loadModule(["worker",t],function(e){r=new e[n](u);while(a.length)c()}),l};t.UIWorkerClient=l,t.WorkerClient=f,t.createWorker=a}),ace.define("ace/placeholder",["require","exports","module","ace/range","ace/lib/event_emitter","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./range").Range,i=e("./lib/event_emitter").EventEmitter,s=e("./lib/oop"),o=function(e,t,n,r,i,s){var o=this;this.length=t,this.session=e,this.doc=e.getDocument(),this.mainClass=i,this.othersClass=s,this.$onUpdate=this.onUpdate.bind(this),this.doc.on("change",this.$onUpdate,!0),this.$others=r,this.$onCursorChange=function(){setTimeout(function(){o.onCursorChange()})},this.$pos=n;var u=e.getUndoManager().$undoStack||e.getUndoManager().$undostack||{length:-1};this.$undoStackDepth=u.length,this.setup(),e.selection.on("changeCursor",this.$onCursorChange)};(function(){s.implement(this,i),this.setup=function(){var e=this,t=this.doc,n=this.session;this.selectionBefore=n.selection.toJSON(),n.selection.inMultiSelectMode&&n.selection.toSingleRange(),this.pos=t.createAnchor(this.$pos.row,this.$pos.column);var i=this.pos;i.$insertRight=!0,i.detach(),i.markerId=n.addMarker(new r(i.row,i.column,i.row,i.column+this.length),this.mainClass,null,!1),this.others=[],this.$others.forEach(function(n){var r=t.createAnchor(n.row,n.column);r.$insertRight=!0,r.detach(),e.others.push(r)}),n.setUndoSelect(!1)},this.showOtherMarkers=function(){if(this.othersActive)return;var e=this.session,t=this;this.othersActive=!0,this.others.forEach(function(n){n.markerId=e.addMarker(new r(n.row,n.column,n.row,n.column+t.length),t.othersClass,null,!1)})},this.hideOtherMarkers=function(){if(!this.othersActive)return;this.othersActive=!1;for(var e=0;e=this.pos.column&&t.start.column<=this.pos.column+this.length+1,s=t.start.column-this.pos.column;this.updateAnchors(e),i&&(this.length+=n);if(i&&!this.session.$fromUndo)if(e.action==="insert")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.insertMergedLines(a,e.lines)}else if(e.action==="remove")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.remove(new r(a.row,a.column,a.row,a.column-n))}this.$updating=!1,this.updateMarkers()},this.updateAnchors=function(e){this.pos.onChange(e);for(var t=this.others.length;t--;)this.others[t].onChange(e);this.updateMarkers()},this.updateMarkers=function(){if(this.$updating)return;var e=this,t=this.session,n=function(n,i){t.removeMarker(n.markerId),n.markerId=t.addMarker(new r(n.row,n.column,n.row,n.column+e.length),i,null,!1)};n(this.pos,this.mainClass);for(var i=this.others.length;i--;)n(this.others[i],this.othersClass)},this.onCursorChange=function(e){if(this.$updating||!this.session)return;var t=this.session.selection.getCursor();t.row===this.pos.row&&t.column>=this.pos.column&&t.column<=this.pos.column+this.length?(this.showOtherMarkers(),this._emit("cursorEnter",e)):(this.hideOtherMarkers(),this._emit("cursorLeave",e))},this.detach=function(){this.session.removeMarker(this.pos&&this.pos.markerId),this.hideOtherMarkers(),this.doc.off("change",this.$onUpdate),this.session.selection.off("changeCursor",this.$onCursorChange),this.session.setUndoSelect(!0),this.session=null},this.cancel=function(){if(this.$undoStackDepth===-1)return;var e=this.session.getUndoManager(),t=(e.$undoStack||e.$undostack).length-this.$undoStackDepth;for(var n=0;n1?e.multiSelect.joinSelections():e.multiSelect.splitIntoLines()},bindKey:{win:"Ctrl-Alt-L",mac:"Ctrl-Alt-L"},readOnly:!0},{name:"splitSelectionIntoLines",description:"Split into lines",exec:function(e){e.multiSelect.splitIntoLines()},readOnly:!0},{name:"alignCursors",description:"Align cursors",exec:function(e){e.alignCursors()},bindKey:{win:"Ctrl-Alt-A",mac:"Ctrl-Alt-A"},scrollIntoView:"cursor"},{name:"findAll",description:"Find all",exec:function(e){e.findAll()},bindKey:{win:"Ctrl-Alt-K",mac:"Ctrl-Alt-G"},scrollIntoView:"cursor",readOnly:!0}],t.multiSelectCommands=[{name:"singleSelection",description:"Single selection",bindKey:"esc",exec:function(e){e.exitMultiSelectMode()},scrollIntoView:"cursor",readOnly:!0,isAvailable:function(e){return e&&e.inMultiSelectMode}}];var r=e("../keyboard/hash_handler").HashHandler;t.keyboardHandler=new r(t.multiSelectCommands)}),ace.define("ace/multi_select",["require","exports","module","ace/range_list","ace/range","ace/selection","ace/mouse/multi_select_handler","ace/lib/event","ace/lib/lang","ace/commands/multi_select_commands","ace/search","ace/edit_session","ace/editor","ace/config"],function(e,t,n){function h(e,t,n){return c.$options.wrap=!0,c.$options.needle=t,c.$options.backwards=n==-1,c.find(e)}function v(e,t){return e.row==t.row&&e.column==t.column}function m(e){if(e.$multiselectOnSessionChange)return;e.$onAddRange=e.$onAddRange.bind(e),e.$onRemoveRange=e.$onRemoveRange.bind(e),e.$onMultiSelect=e.$onMultiSelect.bind(e),e.$onSingleSelect=e.$onSingleSelect.bind(e),e.$multiselectOnSessionChange=t.onSessionChange.bind(e),e.$checkMultiselectChange=e.$checkMultiselectChange.bind(e),e.$multiselectOnSessionChange(e),e.on("changeSession",e.$multiselectOnSessionChange),e.on("mousedown",o),e.commands.addCommands(f.defaultCommands),g(e)}function g(e){function r(t){n&&(e.renderer.setMouseCursor(""),n=!1)}if(!e.textInput)return;var t=e.textInput.getElement(),n=!1;u.addListener(t,"keydown",function(t){var i=t.keyCode==18&&!(t.ctrlKey||t.shiftKey||t.metaKey);e.$blockSelectEnabled&&i?n||(e.renderer.setMouseCursor("crosshair"),n=!0):n&&r()},e),u.addListener(t,"keyup",r,e),u.addListener(t,"blur",r,e)}var r=e("./range_list").RangeList,i=e("./range").Range,s=e("./selection").Selection,o=e("./mouse/multi_select_handler").onMouseDown,u=e("./lib/event"),a=e("./lib/lang"),f=e("./commands/multi_select_commands");t.commands=f.defaultCommands.concat(f.multiSelectCommands);var l=e("./search").Search,c=new l,p=e("./edit_session").EditSession;(function(){this.getSelectionMarkers=function(){return this.$selectionMarkers}}).call(p.prototype),function(){this.ranges=null,this.rangeList=null,this.addRange=function(e,t){if(!e)return;if(!this.inMultiSelectMode&&this.rangeCount===0){var n=this.toOrientedRange();this.rangeList.add(n),this.rangeList.add(e);if(this.rangeList.ranges.length!=2)return this.rangeList.removeAll(),t||this.fromOrientedRange(e);this.rangeList.removeAll(),this.rangeList.add(n),this.$onAddRange(n)}e.cursor||(e.cursor=e.end);var r=this.rangeList.add(e);return this.$onAddRange(e),r.length&&this.$onRemoveRange(r),this.rangeCount>1&&!this.inMultiSelectMode&&(this._signal("multiSelect"),this.inMultiSelectMode=!0,this.session.$undoSelect=!1,this.rangeList.attach(this.session)),t||this.fromOrientedRange(e)},this.toSingleRange=function(e){e=e||this.ranges[0];var t=this.rangeList.removeAll();t.length&&this.$onRemoveRange(t),e&&this.fromOrientedRange(e)},this.substractPoint=function(e){var t=this.rangeList.substractPoint(e);if(t)return this.$onRemoveRange(t),t[0]},this.mergeOverlappingRanges=function(){var e=this.rangeList.merge();e.length&&this.$onRemoveRange(e)},this.$onAddRange=function(e){this.rangeCount=this.rangeList.ranges.length,this.ranges.unshift(e),this._signal("addRange",{range:e})},this.$onRemoveRange=function(e){this.rangeCount=this.rangeList.ranges.length;if(this.rangeCount==1&&this.inMultiSelectMode){var t=this.rangeList.ranges.pop();e.push(t),this.rangeCount=0}for(var n=e.length;n--;){var r=this.ranges.indexOf(e[n]);this.ranges.splice(r,1)}this._signal("removeRange",{ranges:e}),this.rangeCount===0&&this.inMultiSelectMode&&(this.inMultiSelectMode=!1,this._signal("singleSelect"),this.session.$undoSelect=!0,this.rangeList.detach(this.session)),t=t||this.ranges[0],t&&!t.isEqual(this.getRange())&&this.fromOrientedRange(t)},this.$initRangeList=function(){if(this.rangeList)return;this.rangeList=new r,this.ranges=[],this.rangeCount=0},this.getAllRanges=function(){return this.rangeCount?this.rangeList.ranges.concat():[this.getRange()]},this.splitIntoLines=function(){var e=this.ranges.length?this.ranges:[this.getRange()],t=[];for(var n=0;n1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var r=this.session.documentToScreenPosition(this.cursor),s=this.session.documentToScreenPosition(this.anchor),o=this.rectangularRangeBlock(r,s);o.forEach(this.addRange,this)}},this.rectangularRangeBlock=function(e,t,n){var r=[],s=e.column0)g--;if(g>0){var y=0;while(r[y].isEmpty())y++}for(var b=g;b>=y;b--)r[b].isEmpty()&&r.splice(b,1)}return r}}.call(s.prototype);var d=e("./editor").Editor;(function(){this.updateSelectionMarkers=function(){this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.addSelectionMarker=function(e){e.cursor||(e.cursor=e.end);var t=this.getSelectionStyle();return e.marker=this.session.addMarker(e,"ace_selection",t),this.session.$selectionMarkers.push(e),this.session.selectionMarkerCount=this.session.$selectionMarkers.length,e},this.removeSelectionMarker=function(e){if(!e.marker)return;this.session.removeMarker(e.marker);var t=this.session.$selectionMarkers.indexOf(e);t!=-1&&this.session.$selectionMarkers.splice(t,1),this.session.selectionMarkerCount=this.session.$selectionMarkers.length},this.removeSelectionMarkers=function(e){var t=this.session.$selectionMarkers;for(var n=e.length;n--;){var r=e[n];if(!r.marker)continue;this.session.removeMarker(r.marker);var i=t.indexOf(r);i!=-1&&t.splice(i,1)}this.session.selectionMarkerCount=t.length},this.$onAddRange=function(e){this.addSelectionMarker(e.range),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onRemoveRange=function(e){this.removeSelectionMarkers(e.ranges),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onMultiSelect=function(e){if(this.inMultiSelectMode)return;this.inMultiSelectMode=!0,this.setStyle("ace_multiselect"),this.keyBinding.addKeyboardHandler(f.keyboardHandler),this.commands.setDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onSingleSelect=function(e){if(this.session.multiSelect.inVirtualMode)return;this.inMultiSelectMode=!1,this.unsetStyle("ace_multiselect"),this.keyBinding.removeKeyboardHandler(f.keyboardHandler),this.commands.removeDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers(),this._emit("changeSelection")},this.$onMultiSelectExec=function(e){var t=e.command,n=e.editor;if(!n.multiSelect)return;if(!t.multiSelectAction){var r=t.exec(n,e.args||{});n.multiSelect.addRange(n.multiSelect.toOrientedRange()),n.multiSelect.mergeOverlappingRanges()}else t.multiSelectAction=="forEach"?r=n.forEachSelection(t,e.args):t.multiSelectAction=="forEachLine"?r=n.forEachSelection(t,e.args,!0):t.multiSelectAction=="single"?(n.exitMultiSelectMode(),r=t.exec(n,e.args||{})):r=t.multiSelectAction(n,e.args||{});return r},this.forEachSelection=function(e,t,n){if(this.inVirtualSelectionMode)return;var r=n&&n.keepOrder,i=n==1||n&&n.$byLines,o=this.session,u=this.selection,a=u.rangeList,f=(r?u:a).ranges,l;if(!f.length)return e.exec?e.exec(this,t||{}):e(this,t||{});var c=u._eventRegistry;u._eventRegistry={};var h=new s(o);this.inVirtualSelectionMode=!0;for(var p=f.length;p--;){if(i)while(p>0&&f[p].start.row==f[p-1].end.row)p--;h.fromOrientedRange(f[p]),h.index=p,this.selection=o.selection=h;var d=e.exec?e.exec(this,t||{}):e(this,t||{});!l&&d!==undefined&&(l=d),h.toOrientedRange(f[p])}h.detach(),this.selection=o.selection=u,this.inVirtualSelectionMode=!1,u._eventRegistry=c,u.mergeOverlappingRanges(),u.ranges[0]&&u.fromOrientedRange(u.ranges[0]);var v=this.renderer.$scrollAnimation;return this.onCursorChange(),this.onSelectionChange(),v&&v.from==v.to&&this.renderer.animateScrolling(v.from),l},this.exitMultiSelectMode=function(){if(!this.inMultiSelectMode||this.inVirtualSelectionMode)return;this.multiSelect.toSingleRange()},this.getSelectedText=function(){var e="";if(this.inMultiSelectMode&&!this.inVirtualSelectionMode){var t=this.multiSelect.rangeList.ranges,n=[];for(var r=0;r0);u<0&&(u=0),f>=c&&(f=c-1)}var p=this.session.removeFullLines(u,f);p=this.$reAlignText(p,l),this.session.insert({row:u,column:0},p.join("\n")+"\n"),l||(o.start.column=0,o.end.column=p[p.length-1].length),this.selection.setRange(o)}else{s.forEach(function(e){t.substractPoint(e.cursor)});var d=0,v=Infinity,m=n.map(function(t){var n=t.cursor,r=e.getLine(n.row),i=r.substr(n.column).search(/\S/g);return i==-1&&(i=0),n.column>d&&(d=n.column),io?e.insert(r,a.stringRepeat(" ",s-o)):e.remove(new i(r.row,r.column,r.row,r.column-s+o)),t.start.column=t.end.column=d,t.start.row=t.end.row=r.row,t.cursor=t.end}),t.fromOrientedRange(n[0]),this.renderer.updateCursor(),this.renderer.updateBackMarkers()}},this.$reAlignText=function(e,t){function u(e){return a.stringRepeat(" ",e)}function f(e){return e[2]?u(i)+e[2]+u(s-e[2].length+o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function l(e){return e[2]?u(i+s-e[2].length)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function c(e){return e[2]?u(i)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}var n=!0,r=!0,i,s,o;return e.map(function(e){var t=e.match(/(\s*)(.*?)(\s*)([=:].*)/);return t?i==null?(i=t[1].length,s=t[2].length,o=t[3].length,t):(i+s+o!=t[1].length+t[2].length+t[3].length&&(r=!1),i!=t[1].length&&(n=!1),i>t[1].length&&(i=t[1].length),st[3].length&&(o=t[3].length),t):[e]}).map(t?f:n?r?l:f:c)}}).call(d.prototype),t.onSessionChange=function(e){var t=e.session;t&&!t.multiSelect&&(t.$selectionMarkers=[],t.selection.$initRangeList(),t.multiSelect=t.selection),this.multiSelect=t&&t.multiSelect;var n=e.oldSession;n&&(n.multiSelect.off("addRange",this.$onAddRange),n.multiSelect.off("removeRange",this.$onRemoveRange),n.multiSelect.off("multiSelect",this.$onMultiSelect),n.multiSelect.off("singleSelect",this.$onSingleSelect),n.multiSelect.lead.off("change",this.$checkMultiselectChange),n.multiSelect.anchor.off("change",this.$checkMultiselectChange)),t&&(t.multiSelect.on("addRange",this.$onAddRange),t.multiSelect.on("removeRange",this.$onRemoveRange),t.multiSelect.on("multiSelect",this.$onMultiSelect),t.multiSelect.on("singleSelect",this.$onSingleSelect),t.multiSelect.lead.on("change",this.$checkMultiselectChange),t.multiSelect.anchor.on("change",this.$checkMultiselectChange)),t&&this.inMultiSelectMode!=t.selection.inMultiSelectMode&&(t.selection.inMultiSelectMode?this.$onMultiSelect():this.$onSingleSelect())},t.MultiSelect=m,e("./config").defineOptions(d.prototype,"editor",{enableMultiselect:{set:function(e){m(this),e?(this.on("changeSession",this.$multiselectOnSessionChange),this.on("mousedown",o)):(this.off("changeSession",this.$multiselectOnSessionChange),this.off("mousedown",o))},value:!0},enableBlockSelect:{set:function(e){this.$blockSelectEnabled=e},value:!0}})}),ace.define("ace/mode/folding/fold_mode",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../../range").Range,i=t.FoldMode=function(){};(function(){this.foldingStartMarker=null,this.foldingStopMarker=null,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);return this.foldingStartMarker.test(r)?"start":t=="markbeginend"&&this.foldingStopMarker&&this.foldingStopMarker.test(r)?"end":""},this.getFoldWidgetRange=function(e,t,n){return null},this.indentationBlock=function(e,t,n){var i=/\S/,s=e.getLine(t),o=s.search(i);if(o==-1)return;var u=n||s.length,a=e.getLength(),f=t,l=t;while(++tf){var p=e.getLine(l).length;return new r(f,u,l,p)}},this.openingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i+1},u=e.$findClosingBracket(t,o,s);if(!u)return;var a=e.foldWidgets[u.row];return a==null&&(a=e.getFoldWidget(u.row)),a=="start"&&u.row>o.row&&(u.row--,u.column=e.getLine(u.row).length),r.fromPoints(o,u)},this.closingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i},u=e.$findOpeningBracket(t,o);if(!u)return;return u.column++,o.column--,r.fromPoints(u,o)}}).call(i.prototype)}),ace.define("ace/ext/error_marker",["require","exports","module","ace/line_widgets","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e,t,n){var r=0,i=e.length-1;while(r<=i){var s=r+i>>1,o=n(t,e[s]);if(o>0)r=s+1;else{if(!(o<0))return s;i=s-1}}return-(r+1)}function u(e,t,n){var r=e.getAnnotations().sort(s.comparePoints);if(!r.length)return;var i=o(r,{row:t,column:-1},s.comparePoints);i<0&&(i=-i-1),i>=r.length?i=n>0?0:r.length-1:i===0&&n<0&&(i=r.length-1);var u=r[i];if(!u||!n)return;if(u.row===t){do u=r[i+=n];while(u&&u.row===t);if(!u)return r.slice()}var a=[];t=u.row;do a[n<0?"unshift":"push"](u),u=r[i+=n];while(u&&u.row==t);return a.length&&a}var r=e("../line_widgets").LineWidgets,i=e("../lib/dom"),s=e("../range").Range;t.showErrorMarker=function(e,t){var n=e.session;n.widgetManager||(n.widgetManager=new r(n),n.widgetManager.attach(e));var s=e.getCursorPosition(),o=s.row,a=n.widgetManager.getWidgetsAtRow(o).filter(function(e){return e.type=="errorMarker"})[0];a?a.destroy():o-=t;var f=u(n,o,t),l;if(f){var c=f[0];s.column=(c.pos&&typeof c.column!="number"?c.pos.sc:c.column)||0,s.row=c.row,l=e.renderer.$gutterLayer.$annotations[s.row]}else{if(a)return;l={text:["Looks good!"],className:"ace_ok"}}e.session.unfold(s.row),e.selection.moveToPosition(s);var h={row:s.row,fixedWidth:!0,coverGutter:!0,el:i.createElement("div"),type:"errorMarker"},p=h.el.appendChild(i.createElement("div")),d=h.el.appendChild(i.createElement("div"));d.className="error_widget_arrow "+l.className;var v=e.renderer.$cursorLayer.getPixelPosition(s).left;d.style.left=v+e.renderer.gutterWidth-5+"px",h.el.className="error_widget_wrapper",p.className="error_widget "+l.className,p.innerHTML=l.text.join("
"),p.appendChild(i.createElement("div"));var m=function(e,t,n){if(t===0&&(n==="esc"||n==="return"))return h.destroy(),{command:"null"}};h.destroy=function(){if(e.$mouseHandler.isMousePressed)return;e.keyBinding.removeKeyboardHandler(m),n.widgetManager.removeLineWidget(h),e.off("changeSelection",h.destroy),e.off("changeSession",h.destroy),e.off("mouseup",h.destroy),e.off("change",h.destroy)},e.keyBinding.addKeyboardHandler(m),e.on("changeSelection",h.destroy),e.on("changeSession",h.destroy),e.on("mouseup",h.destroy),e.on("change",h.destroy),e.session.widgetManager.addLineWidget(h),h.el.onmousedown=e.focus.bind(e),e.renderer.scrollCursorIntoView(null,.5,{bottom:h.el.offsetHeight})},i.importCssString("\n .error_widget_wrapper {\n background: inherit;\n color: inherit;\n border:none\n }\n .error_widget {\n border-top: solid 2px;\n border-bottom: solid 2px;\n margin: 5px 0;\n padding: 10px 40px;\n white-space: pre-wrap;\n }\n .error_widget.ace_error, .error_widget_arrow.ace_error{\n border-color: #ff5a5a\n }\n .error_widget.ace_warning, .error_widget_arrow.ace_warning{\n border-color: #F1D817\n }\n .error_widget.ace_info, .error_widget_arrow.ace_info{\n border-color: #5a5a5a\n }\n .error_widget.ace_ok, .error_widget_arrow.ace_ok{\n border-color: #5aaa5a\n }\n .error_widget_arrow {\n position: absolute;\n border: solid 5px;\n border-top-color: transparent!important;\n border-right-color: transparent!important;\n border-left-color: transparent!important;\n top: -5px;\n }\n","error_marker.css",!1)}),ace.define("ace/ace",["require","exports","module","ace/lib/dom","ace/lib/event","ace/range","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/worker/worker_client","ace/keyboard/hash_handler","ace/placeholder","ace/multi_select","ace/mode/folding/fold_mode","ace/theme/textmate","ace/ext/error_marker","ace/config","ace/loader_build"],function(e,t,n){"use strict";e("./loader_build")(t);var r=e("./lib/dom"),i=e("./lib/event"),s=e("./range").Range,o=e("./editor").Editor,u=e("./edit_session").EditSession,a=e("./undomanager").UndoManager,f=e("./virtual_renderer").VirtualRenderer;e("./worker/worker_client"),e("./keyboard/hash_handler"),e("./placeholder"),e("./multi_select"),e("./mode/folding/fold_mode"),e("./theme/textmate"),e("./ext/error_marker"),t.config=e("./config"),t.edit=function(e,n){if(typeof e=="string"){var s=e;e=document.getElementById(s);if(!e)throw new Error("ace.edit can't find div #"+s)}if(e&&e.env&&e.env.editor instanceof o)return e.env.editor;var u="";if(e&&/input|textarea/i.test(e.tagName)){var a=e;u=a.value,e=r.createElement("pre"),a.parentNode.replaceChild(e,a)}else e&&(u=e.textContent,e.innerHTML="");var l=t.createEditSession(u),c=new o(new f(e),l,n),h={document:l,editor:c,onResize:c.resize.bind(c,null)};return a&&(h.textarea=a),i.addListener(window,"resize",h.onResize),c.on("destroy",function(){i.removeListener(window,"resize",h.onResize),h.editor.container.env=null}),c.container.env=c.env=h,c},t.createEditSession=function(e,t){var n=new u(e,t);return n.setUndoManager(new a),n},t.Range=s,t.Editor=o,t.EditSession=u,t.UndoManager=a,t.VirtualRenderer=f,t.version=t.config.version}); (function() { + ace.require(["ace/ace"], function(a) { + if (a) { + a.config.init(true); + a.define = ace.define; + } + if (!window.ace) + window.ace = a; + for (var key in a) if (a.hasOwnProperty(key)) + window.ace[key] = a[key]; + window.ace["default"] = window.ace; + if (typeof module == "object" && typeof exports == "object" && module) { + module.exports = window.ace; + } + }); + })(); + \ No newline at end of file diff --git a/web/static/js/demo-theme.min.js b/web/static/js/demo-theme.min.js new file mode 100644 index 0000000..89ed4fb --- /dev/null +++ b/web/static/js/demo-theme.min.js @@ -0,0 +1,9 @@ +/*! +* Tabler v1.0.0-beta16 (https://tabler.io) +* @version 1.0.0-beta16 +* @link https://tabler.io +* Copyright 2018-2022 The Tabler Authors +* Copyright 2018-2022 codecalm.net Paweł Kuna +* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) +*/ +!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e,t="tablerTheme",n=new Proxy(new URLSearchParams(window.location.search),{get:function(e,t){return e.get(t)}});if(n.theme)localStorage.setItem(t,n.theme),e=n.theme;else{var o=localStorage.getItem(t);e=o||"light"}document.body.classList.remove("theme-dark","theme-light"),document.body.classList.add("theme-".concat(e))})); \ No newline at end of file diff --git a/web/static/js/demo.min.js b/web/static/js/demo.min.js new file mode 100644 index 0000000..3630fea --- /dev/null +++ b/web/static/js/demo.min.js @@ -0,0 +1,9 @@ +/*! +* Tabler v1.0.0-beta16 (https://tabler.io) +* @version 1.0.0-beta16 +* @link https://tabler.io +* Copyright 2018-2022 The Tabler Authors +* Copyright 2018-2022 codecalm.net Paweł Kuna +* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) +*/ +!function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";function t(t,r){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==r)return;var n,o,a=[],l=!0,i=!1;try{for(r=r.call(t);!(l=(n=r.next()).done)&&(a.push(n.value),!e||a.length!==e);l=!0);}catch(t){i=!0,o=t}finally{try{l||null==r.return||r.return()}finally{if(i)throw o}}return a}(t,r)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return e(t,r)}(t,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r'+a+""}).then(function(a){return''+a+""}).then(function(a){return"data:image/svg+xml;charset=utf-8,"+a})}function m(){function a(){var a="application/font-woff",b="image/jpeg";return{woff:a,woff2:a,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:b,jpeg:b,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml"}}function b(a){var b=/\.([^\.\/]*?)$/g.exec(a);return b?b[1]:""}function c(c){var d=b(c).toLowerCase();return a()[d]||""}function d(a){return a.search(/^(data:)/)!==-1}function e(a){return new Promise(function(b){for(var c=window.atob(a.toDataURL().split(",")[1]),d=c.length,e=new Uint8Array(d),f=0;f1?t-1:0),n=1;n'),this.element.appendChild(e));var l=e.getElementsByTagName("span")[0];return l&&(null!=l.textContent?l.textContent=this.options.dictFallbackMessage:null!=l.innerText&&(l.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(e,t,i,n){var r={srcX:0,srcY:0,srcWidth:e.width,srcHeight:e.height},a=e.width/e.height;null==t&&null==i?(t=r.srcWidth,i=r.srcHeight):null==t?t=i*a:null==i&&(i=t/a);var o=(t=Math.min(t,r.srcWidth))/(i=Math.min(i,r.srcHeight));if(r.srcWidth>t||r.srcHeight>i)if("crop"===n)a>o?(r.srcHeight=e.height,r.srcWidth=r.srcHeight*o):(r.srcWidth=e.width,r.srcHeight=r.srcWidth/o);else{if("contain"!==n)throw new Error("Unknown resizeMethod '".concat(n,"'"));a>o?i=t/a:t=i*a}return r.srcX=(e.width-r.srcWidth)/2,r.srcY=(e.height-r.srcHeight)/2,r.trgWidth=t,r.trgHeight=i,r},transformFile:function(e,t){return(this.options.resizeWidth||this.options.resizeHeight)&&e.type.match(/image.*/)?this.resizeImage(e,this.options.resizeWidth,this.options.resizeHeight,this.options.resizeMethod,t):t(e)},previewTemplate:e('
'),drop:function(e){return this.element.classList.remove("dz-drag-hover")},dragstart:function(e){},dragend:function(e){return this.element.classList.remove("dz-drag-hover")},dragenter:function(e){return this.element.classList.add("dz-drag-hover")},dragover:function(e){return this.element.classList.add("dz-drag-hover")},dragleave:function(e){return this.element.classList.remove("dz-drag-hover")},paste:function(e){},reset:function(){return this.element.classList.remove("dz-started")},addedfile:function(e){if(this.element===this.previewsContainer&&this.element.classList.add("dz-started"),this.previewsContainer&&!this.options.disablePreviews){var t=this;e.previewElement=f.createElement(this.options.previewTemplate.trim()),e.previewTemplate=e.previewElement,this.previewsContainer.appendChild(e.previewElement);var i=!0,n=!1,r=void 0;try{for(var a,o=e.previewElement.querySelectorAll("[data-dz-name]")[Symbol.iterator]();!(i=(a=o.next()).done);i=!0){var l=a.value;l.textContent=e.name}}catch(e){n=!0,r=e}finally{try{i||null==o.return||o.return()}finally{if(n)throw r}}var s=!0,u=!1,c=void 0;try{for(var d,h=e.previewElement.querySelectorAll("[data-dz-size]")[Symbol.iterator]();!(s=(d=h.next()).done);s=!0)(l=d.value).innerHTML=this.filesize(e.size)}catch(e){u=!0,c=e}finally{try{s||null==h.return||h.return()}finally{if(u)throw c}}this.options.addRemoveLinks&&(e._removeLink=f.createElement(''.concat(this.options.dictRemoveFile,"")),e.previewElement.appendChild(e._removeLink));var p=function(i){var n=t;if(i.preventDefault(),i.stopPropagation(),e.status===f.UPLOADING)return f.confirm(t.options.dictCancelUploadConfirmation,(function(){return n.removeFile(e)}));var r=t;return t.options.dictRemoveFileConfirmation?f.confirm(t.options.dictRemoveFileConfirmation,(function(){return r.removeFile(e)})):t.removeFile(e)},m=!0,v=!1,y=void 0;try{for(var g,b=e.previewElement.querySelectorAll("[data-dz-remove]")[Symbol.iterator]();!(m=(g=b.next()).done);m=!0){g.value.addEventListener("click",p)}}catch(e){v=!0,y=e}finally{try{m||null==b.return||b.return()}finally{if(v)throw y}}}},removedfile:function(e){return null!=e.previewElement&&null!=e.previewElement.parentNode&&e.previewElement.parentNode.removeChild(e.previewElement),this._updateMaxFilesReachedClass()},thumbnail:function(e,t){if(e.previewElement){e.previewElement.classList.remove("dz-file-preview");var i=!0,n=!1,r=void 0;try{for(var a,o=e.previewElement.querySelectorAll("[data-dz-thumbnail]")[Symbol.iterator]();!(i=(a=o.next()).done);i=!0){var l=a.value;l.alt=e.name,l.src=t}}catch(e){n=!0,r=e}finally{try{i||null==o.return||o.return()}finally{if(n)throw r}}return setTimeout((function(){return e.previewElement.classList.add("dz-image-preview")}),1)}},error:function(e,t){if(e.previewElement){e.previewElement.classList.add("dz-error"),"string"!=typeof t&&t.error&&(t=t.error);var i=!0,n=!1,r=void 0;try{for(var a,o=e.previewElement.querySelectorAll("[data-dz-errormessage]")[Symbol.iterator]();!(i=(a=o.next()).done);i=!0){a.value.textContent=t}}catch(e){n=!0,r=e}finally{try{i||null==o.return||o.return()}finally{if(n)throw r}}}},errormultiple:function(){},processing:function(e){if(e.previewElement&&(e.previewElement.classList.add("dz-processing"),e._removeLink))return e._removeLink.innerHTML=this.options.dictCancelUpload},processingmultiple:function(){},uploadprogress:function(e,t,i){var n=!0,r=!1,a=void 0;if(e.previewElement)try{for(var o,l=e.previewElement.querySelectorAll("[data-dz-uploadprogress]")[Symbol.iterator]();!(n=(o=l.next()).done);n=!0){var s=o.value;"PROGRESS"===s.nodeName?s.value=t:s.style.width="".concat(t,"%")}}catch(e){r=!0,a=e}finally{try{n||null==l.return||l.return()}finally{if(r)throw a}}},totaluploadprogress:function(){},sending:function(){},sendingmultiple:function(){},success:function(e){if(e.previewElement)return e.previewElement.classList.add("dz-success")},successmultiple:function(){},canceled:function(e){return this.emit("error",e,this.options.dictUploadCanceled)},canceledmultiple:function(){},complete:function(e){if(e._removeLink&&(e._removeLink.innerHTML=this.options.dictRemoveFile),e.previewElement)return e.previewElement.classList.add("dz-complete")},completemultiple:function(){},maxfilesexceeded:function(){},maxfilesreached:function(){},queuecomplete:function(){},addedfiles:function(){}},f=function(n){"use strict";function o(n,r){var l,c,d,h;if(i(this,o),(l=s(this,(c=o,a(c)).call(this))).element=n,l.clickableElements=[],l.listeners=[],l.files=[],"string"==typeof l.element&&(l.element=document.querySelector(l.element)),!l.element||null==l.element.nodeType)throw new Error("Invalid dropzone element.");if(l.element.dropzone)throw new Error("Dropzone already attached.");o.instances.push(t(l)),l.element.dropzone=t(l);var f=null!=(h=o.optionsForElement(l.element))?h:{};if(l.options=e(u)(!0,{},p,f,null!=r?r:{}),l.options.previewTemplate=l.options.previewTemplate.replace(/\n*/g,""),l.options.forceFallback||!o.isBrowserSupported())return s(l,l.options.fallback.call(t(l)));if(null==l.options.url&&(l.options.url=l.element.getAttribute("action")),!l.options.url)throw new Error("No URL provided.");if(l.options.acceptedFiles&&l.options.acceptedMimeTypes)throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated.");if(l.options.uploadMultiple&&l.options.chunking)throw new Error("You cannot set both: uploadMultiple and chunking.");if(l.options.binaryBody&&l.options.uploadMultiple)throw new Error("You cannot set both: binaryBody and uploadMultiple.");return l.options.acceptedMimeTypes&&(l.options.acceptedFiles=l.options.acceptedMimeTypes,delete l.options.acceptedMimeTypes),null!=l.options.renameFilename&&(l.options.renameFile=function(e){return l.options.renameFilename.call(t(l),e.name,e)}),"string"==typeof l.options.method&&(l.options.method=l.options.method.toUpperCase()),(d=l.getExistingFallback())&&d.parentNode&&d.parentNode.removeChild(d),!1!==l.options.previewsContainer&&(l.options.previewsContainer?l.previewsContainer=o.getElement(l.options.previewsContainer,"previewsContainer"):l.previewsContainer=l.element),l.options.clickable&&(!0===l.options.clickable?l.clickableElements=[l.element]:l.clickableElements=o.getElements(l.options.clickable,"clickable")),l.init(),l}return l(o,n),r(o,[{key:"getAcceptedFiles",value:function(){return this.files.filter((function(e){return e.accepted})).map((function(e){return e}))}},{key:"getRejectedFiles",value:function(){return this.files.filter((function(e){return!e.accepted})).map((function(e){return e}))}},{key:"getFilesWithStatus",value:function(e){return this.files.filter((function(t){return t.status===e})).map((function(e){return e}))}},{key:"getQueuedFiles",value:function(){return this.getFilesWithStatus(o.QUEUED)}},{key:"getUploadingFiles",value:function(){return this.getFilesWithStatus(o.UPLOADING)}},{key:"getAddedFiles",value:function(){return this.getFilesWithStatus(o.ADDED)}},{key:"getActiveFiles",value:function(){return this.files.filter((function(e){return e.status===o.UPLOADING||e.status===o.QUEUED})).map((function(e){return e}))}},{key:"init",value:function(){var e=this,t=this,i=this,n=this,r=this,a=this,l=this,s=this,u=this,c=this,d=this;if("form"===this.element.tagName&&this.element.setAttribute("enctype","multipart/form-data"),this.element.classList.contains("dropzone")&&!this.element.querySelector(".dz-message")&&this.element.appendChild(o.createElement('
"))),this.clickableElements.length){var h=this,p=function(){var e=h;h.hiddenFileInput&&h.hiddenFileInput.parentNode.removeChild(h.hiddenFileInput),h.hiddenFileInput=document.createElement("input"),h.hiddenFileInput.setAttribute("type","file"),(null===h.options.maxFiles||h.options.maxFiles>1)&&h.hiddenFileInput.setAttribute("multiple","multiple"),h.hiddenFileInput.className="dz-hidden-input",null!==h.options.acceptedFiles&&h.hiddenFileInput.setAttribute("accept",h.options.acceptedFiles),null!==h.options.capture&&h.hiddenFileInput.setAttribute("capture",h.options.capture),h.hiddenFileInput.setAttribute("tabindex","-1"),h.hiddenFileInput.style.visibility="hidden",h.hiddenFileInput.style.position="absolute",h.hiddenFileInput.style.top="0",h.hiddenFileInput.style.left="0",h.hiddenFileInput.style.height="0",h.hiddenFileInput.style.width="0",o.getElement(h.options.hiddenInputContainer,"hiddenInputContainer").appendChild(h.hiddenFileInput),h.hiddenFileInput.addEventListener("change",(function(){var t=e.hiddenFileInput.files,i=!0,n=!1,r=void 0;if(t.length)try{for(var a,o=t[Symbol.iterator]();!(i=(a=o.next()).done);i=!0){var l=a.value;e.addFile(l)}}catch(e){n=!0,r=e}finally{try{i||null==o.return||o.return()}finally{if(n)throw r}}e.emit("addedfiles",t),p()}))};p()}this.URL=null!==window.URL?window.URL:window.webkitURL;var f=!0,m=!1,v=void 0;try{for(var y,g=this.events[Symbol.iterator]();!(f=(y=g.next()).done);f=!0){var b=y.value;this.on(b,this.options[b])}}catch(e){m=!0,v=e}finally{try{f||null==g.return||g.return()}finally{if(m)throw v}}this.on("uploadprogress",(function(){return e.updateTotalUploadProgress()})),this.on("removedfile",(function(){return t.updateTotalUploadProgress()})),this.on("canceled",(function(e){return i.emit("complete",e)})),this.on("complete",(function(e){var t=n;if(0===n.getAddedFiles().length&&0===n.getUploadingFiles().length&&0===n.getQueuedFiles().length)return setTimeout((function(){return t.emit("queuecomplete")}),0)}));var k=function(e){if(function(e){if(e.dataTransfer.types)for(var t=0;t")),i+='');var n=o.createElement(i);return"FORM"!==this.element.tagName?(t=o.createElement('
'))).appendChild(n):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=t?t:n}},{key:"getExistingFallback",value:function(){var e=function(e){var t=!0,i=!1,n=void 0;try{for(var r,a=e[Symbol.iterator]();!(t=(r=a.next()).done);t=!0){var o=r.value;if(/(^| )fallback($| )/.test(o.className))return o}}catch(e){i=!0,n=e}finally{try{t||null==a.return||a.return()}finally{if(i)throw n}}},t=!0,i=!1,n=void 0;try{for(var r,a=["div","form"][Symbol.iterator]();!(t=(r=a.next()).done);t=!0){var o,l=r.value;if(o=e(this.element.getElementsByTagName(l)))return o}}catch(e){i=!0,n=e}finally{try{t||null==a.return||a.return()}finally{if(i)throw n}}}},{key:"setupEventListeners",value:function(){return this.listeners.map((function(e){return function(){var t=[];for(var i in e.events){var n=e.events[i];t.push(e.element.addEventListener(i,n,!1))}return t}()}))}},{key:"removeEventListeners",value:function(){return this.listeners.map((function(e){return function(){var t=[];for(var i in e.events){var n=e.events[i];t.push(e.element.removeEventListener(i,n,!1))}return t}()}))}},{key:"disable",value:function(){var e=this;return this.clickableElements.forEach((function(e){return e.classList.remove("dz-clickable")})),this.removeEventListeners(),this.disabled=!0,this.files.map((function(t){return e.cancelUpload(t)}))}},{key:"enable",value:function(){return delete this.disabled,this.clickableElements.forEach((function(e){return e.classList.add("dz-clickable")})),this.setupEventListeners()}},{key:"filesize",value:function(e){var t=0,i="b";if(e>0){for(var n=["tb","gb","mb","kb","b"],r=0;r=Math.pow(this.options.filesizeBase,4-r)/10){t=e/Math.pow(this.options.filesizeBase,4-r),i=a;break}}t=Math.round(10*t)/10}return"".concat(t," ").concat(this.options.dictFileSizeUnits[i])}},{key:"_updateMaxFilesReachedClass",value:function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit("maxfilesreached",this.files),this.element.classList.add("dz-max-files-reached")):this.element.classList.remove("dz-max-files-reached")}},{key:"drop",value:function(e){if(e.dataTransfer){this.emit("drop",e);for(var t=[],i=0;i0){var n=!0,r=!1,o=void 0;try{for(var l,s=i[Symbol.iterator]();!(n=(l=s.next()).done);n=!0){var u=l.value,c=e;u.isFile?u.file((function(e){if(!c.options.ignoreHiddenFiles||"."!==e.name.substring(0,1))return e.fullPath="".concat(t,"/").concat(e.name),c.addFile(e)})):u.isDirectory&&e._addFilesFromDirectory(u,"".concat(t,"/").concat(u.name))}}catch(e){r=!0,o=e}finally{try{n||null==s.return||s.return()}finally{if(r)throw o}}a()}return null}),r)};return a()}},{key:"accept",value:function(e,t){this.options.maxFilesize&&e.size>1048576*this.options.maxFilesize?t(this.options.dictFileTooBig.replace("{{filesize}}",Math.round(e.size/1024/10.24)/100).replace("{{maxFilesize}}",this.options.maxFilesize)):o.isValidFile(e,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(t(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}",this.options.maxFiles)),this.emit("maxfilesexceeded",e)):this.options.accept.call(this,e,t):t(this.options.dictInvalidFileType)}},{key:"addFile",value:function(e){var t=this;e.upload={uuid:o.uuidv4(),progress:0,total:e.size,bytesSent:0,filename:this._renameFile(e)},this.files.push(e),e.status=o.ADDED,this.emit("addedfile",e),this._enqueueThumbnail(e),this.accept(e,(function(i){i?(e.accepted=!1,t._errorProcessing([e],i)):(e.accepted=!0,t.options.autoQueue&&t.enqueueFile(e)),t._updateMaxFilesReachedClass()}))}},{key:"enqueueFiles",value:function(e){var t=!0,i=!1,n=void 0;try{for(var r,a=e[Symbol.iterator]();!(t=(r=a.next()).done);t=!0){var o=r.value;this.enqueueFile(o)}}catch(e){i=!0,n=e}finally{try{t||null==a.return||a.return()}finally{if(i)throw n}}return null}},{key:"enqueueFile",value:function(e){if(e.status!==o.ADDED||!0!==e.accepted)throw new Error("This file can't be queued because it has already been processed or was rejected.");var t=this;if(e.status=o.QUEUED,this.options.autoProcessQueue)return setTimeout((function(){return t.processQueue()}),0)}},{key:"_enqueueThumbnail",value:function(e){if(this.options.createImageThumbnails&&e.type.match(/image.*/)&&e.size<=1048576*this.options.maxThumbnailFilesize){var t=this;return this._thumbnailQueue.push(e),setTimeout((function(){return t._processThumbnailQueue()}),0)}}},{key:"_processThumbnailQueue",value:function(){var e=this;if(!this._processingThumbnail&&0!==this._thumbnailQueue.length){this._processingThumbnail=!0;var t=this._thumbnailQueue.shift();return this.createThumbnail(t,this.options.thumbnailWidth,this.options.thumbnailHeight,this.options.thumbnailMethod,!0,(function(i){return e.emit("thumbnail",t,i),e._processingThumbnail=!1,e._processThumbnailQueue()}))}}},{key:"removeFile",value:function(e){if(e.status===o.UPLOADING&&this.cancelUpload(e),this.files=m(this.files,e),this.emit("removedfile",e),0===this.files.length)return this.emit("reset")}},{key:"removeAllFiles",value:function(e){null==e&&(e=!1);var t=!0,i=!1,n=void 0;try{for(var r,a=this.files.slice()[Symbol.iterator]();!(t=(r=a.next()).done);t=!0){var l=r.value;(l.status!==o.UPLOADING||e)&&this.removeFile(l)}}catch(e){i=!0,n=e}finally{try{t||null==a.return||a.return()}finally{if(i)throw n}}return null}},{key:"resizeImage",value:function(e,t,i,n,r){var a=this;return this.createThumbnail(e,t,i,n,!0,(function(t,i){if(null==i)return r(e);var n=a.options.resizeMimeType;null==n&&(n=e.type);var l=i.toDataURL(n,a.options.resizeQuality);return"image/jpeg"!==n&&"image/jpg"!==n||(l=g.restore(e.dataURL,l)),r(o.dataURItoBlob(l))}))}},{key:"createThumbnail",value:function(e,t,i,n,r,a){var o=this,l=new FileReader;l.onload=function(){e.dataURL=l.result,"image/svg+xml"!==e.type?o.createThumbnailFromUrl(e,t,i,n,r,a):null!=a&&a(l.result)},l.readAsDataURL(e)}},{key:"displayExistingFile",value:function(e,t,i,n,r){var a=void 0===r||r;if(this.emit("addedfile",e),this.emit("complete",e),a){var o=this;e.dataURL=t,this.createThumbnailFromUrl(e,this.options.thumbnailWidth,this.options.thumbnailHeight,this.options.thumbnailMethod,this.options.fixOrientation,(function(t){o.emit("thumbnail",e,t),i&&i()}),n)}else this.emit("thumbnail",e,t),i&&i()}},{key:"createThumbnailFromUrl",value:function(e,t,i,n,r,a,o){var l=this,s=document.createElement("img");return o&&(s.crossOrigin=o),r="from-image"!=getComputedStyle(document.body).imageOrientation&&r,s.onload=function(){var o=l,u=function(e){return e(1)};return"undefined"!=typeof EXIF&&null!==EXIF&&r&&(u=function(e){return EXIF.getData(s,(function(){return e(EXIF.getTag(this,"Orientation"))}))}),u((function(r){e.width=s.width,e.height=s.height;var l=o.options.resize.call(o,e,t,i,n),u=document.createElement("canvas"),c=u.getContext("2d");switch(u.width=l.trgWidth,u.height=l.trgHeight,r>4&&(u.width=l.trgHeight,u.height=l.trgWidth),r){case 2:c.translate(u.width,0),c.scale(-1,1);break;case 3:c.translate(u.width,u.height),c.rotate(Math.PI);break;case 4:c.translate(0,u.height),c.scale(1,-1);break;case 5:c.rotate(.5*Math.PI),c.scale(1,-1);break;case 6:c.rotate(.5*Math.PI),c.translate(0,-u.width);break;case 7:c.rotate(.5*Math.PI),c.translate(u.height,-u.width),c.scale(-1,1);break;case 8:c.rotate(-.5*Math.PI),c.translate(-u.height,0)}y(c,s,null!=l.srcX?l.srcX:0,null!=l.srcY?l.srcY:0,l.srcWidth,l.srcHeight,null!=l.trgX?l.trgX:0,null!=l.trgY?l.trgY:0,l.trgWidth,l.trgHeight);var d=u.toDataURL("image/png");if(null!=a)return a(d,u)}))},null!=a&&(s.onerror=a),s.src=e.dataURL}},{key:"processQueue",value:function(){var e=this.options.parallelUploads,t=this.getUploadingFiles().length,i=t;if(!(t>=e)){var n=this.getQueuedFiles();if(n.length>0){if(this.options.uploadMultiple)return this.processFiles(n.slice(0,e-t));for(;i1?t-1:0),n=1;nt.options.chunkSize),e[0].upload.totalChunkCount=Math.ceil(n.size/t.options.chunkSize)}if(e[0].upload.chunked){var r=t,a=t,l=e[0];n=i[0];l.upload.chunks=[];var s=function(){for(var t=0;void 0!==l.upload.chunks[t];)t++;if(!(t>=l.upload.totalChunkCount)){0;var i=t*r.options.chunkSize,a=Math.min(i+r.options.chunkSize,n.size),s={name:r._getParamName(0),data:n.webkitSlice?n.webkitSlice(i,a):n.slice(i,a),filename:l.upload.filename,chunkIndex:t};l.upload.chunks[t]={file:l,index:t,dataBlock:s,status:o.UPLOADING,progress:0,retries:0},r._uploadData(e,[s])}};if(l.upload.finishedChunkUpload=function(t,i){var n=a,r=!0;t.status=o.SUCCESS,t.dataBlock=null,t.response=t.xhr.responseText,t.responseHeaders=t.xhr.getAllResponseHeaders(),t.xhr=null;for(var u=0;u=o;l?a++:a--)r[a]=t.charCodeAt(a);return new Blob([n],{type:i})};var m=function(e,t){return e.filter((function(e){return e!==t})).map((function(e){return e}))},v=function(e){return e.replace(/[\-_](\w)/g,(function(e){return e.charAt(1).toUpperCase()}))};f.createElement=function(e){var t=document.createElement("div");return t.innerHTML=e,t.childNodes[0]},f.elementInside=function(e,t){if(e===t)return!0;for(;e=e.parentNode;)if(e===t)return!0;return!1},f.getElement=function(e,t){var i;if("string"==typeof e?i=document.querySelector(e):null!=e.nodeType&&(i=e),null==i)throw new Error("Invalid `".concat(t,"` option provided. Please provide a CSS selector or a plain HTML element."));return i},f.getElements=function(e,t){var i,n;if(e instanceof Array){n=[];try{var r=!0,a=!1,o=void 0;try{for(var l=e[Symbol.iterator]();!(r=(s=l.next()).done);r=!0)i=s.value,n.push(this.getElement(i,t))}catch(e){a=!0,o=e}finally{try{r||null==l.return||l.return()}finally{if(a)throw o}}}catch(e){n=null}}else if("string"==typeof e){n=[];r=!0,a=!1,o=void 0;try{var s;for(l=document.querySelectorAll(e)[Symbol.iterator]();!(r=(s=l.next()).done);r=!0)i=s.value,n.push(i)}catch(e){a=!0,o=e}finally{try{r||null==l.return||l.return()}finally{if(a)throw o}}}else null!=e.nodeType&&(n=[e]);if(null==n||!n.length)throw new Error("Invalid `".concat(t,"` option provided. Please provide a CSS selector, a plain HTML element or a list of those."));return n},f.confirm=function(e,t,i){return window.confirm(e)?t():null!=i?i():void 0},f.isValidFile=function(e,t){if(!t)return!0;t=t.split(",");var i=e.type,n=i.replace(/\/.*$/,""),r=!0,a=!1,o=void 0;try{for(var l,s=t[Symbol.iterator]();!(r=(l=s.next()).done);r=!0){var u=l.value;if("."===(u=u.trim()).charAt(0)){if(-1!==e.name.toLowerCase().indexOf(u.toLowerCase(),e.name.length-u.length))return!0}else if(/\/\*$/.test(u)){if(n===u.replace(/\/.*$/,""))return!0}else if(i===u)return!0}}catch(e){a=!0,o=e}finally{try{r||null==s.return||s.return()}finally{if(a)throw o}}return!1},"undefined"!=typeof jQuery&&null!==jQuery&&(jQuery.fn.dropzone=function(e){return this.each((function(){return new f(this,e)}))}),f.ADDED="added",f.QUEUED="queued",f.ACCEPTED=f.QUEUED,f.UPLOADING="uploading",f.PROCESSING=f.UPLOADING,f.CANCELED="canceled",f.ERROR="error",f.SUCCESS="success";var y=function(e,t,i,n,r,a,o,l,s,u){var c=function(e){e.naturalWidth;var t=e.naturalHeight,i=document.createElement("canvas");i.width=1,i.height=t;var n=i.getContext("2d");n.drawImage(e,0,0);for(var r=n.getImageData(1,0,1,t).data,a=0,o=t,l=t;l>a;)0===r[4*(l-1)+3]?o=l:a=l,l=o+a>>1;var s=l/t;return 0===s?1:s}(t);return e.drawImage(t,i,n,r,a,o,l,s,u/c)},g=function(){"use strict";function e(){i(this,e)}return r(e,null,[{key:"initClass",value:function(){this.KEY_STR="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}},{key:"encode64",value:function(e){for(var t="",i=void 0,n=void 0,r="",a=void 0,o=void 0,l=void 0,s="",u=0;a=(i=e[u++])>>2,o=(3&i)<<4|(n=e[u++])>>4,l=(15&n)<<2|(r=e[u++])>>6,s=63&r,isNaN(n)?l=s=64:isNaN(r)&&(s=64),t=t+this.KEY_STR.charAt(a)+this.KEY_STR.charAt(o)+this.KEY_STR.charAt(l)+this.KEY_STR.charAt(s),i=n=r="",a=o=l=s="",ue.length)break}return i}},{key:"decode64",value:function(e){var t=void 0,i=void 0,n="",r=void 0,a=void 0,o="",l=0,s=[];for(/[^A-Za-z0-9\+\/\=]/g.exec(e)&&console.warn("There were invalid base64 characters in the input text.\nValid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\nExpect errors in decoding."),e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");t=this.KEY_STR.indexOf(e.charAt(l++))<<2|(r=this.KEY_STR.indexOf(e.charAt(l++)))>>4,i=(15&r)<<4|(a=this.KEY_STR.indexOf(e.charAt(l++)))>>2,n=(3&a)<<6|(o=this.KEY_STR.indexOf(e.charAt(l++))),s.push(t),64!==a&&s.push(i),64!==o&&s.push(n),t=i=n="",r=a=o="",l18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px sans-serif";var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=0)o=r*t.length;else for(var c=0;c>1)%2;a.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",r[l]+":0",i[1-s]+":auto",r[1-l]+":auto",""].join("!important;"),t.appendChild(a),n.push(a)}return n}(e,a),a,o);if(s)return s(t,n,i),!0}return!1}function Jt(t){return"CANVAS"===t.nodeName.toUpperCase()}var Qt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,te=[],ee=r.browser.firefox&&+r.browser.version.split(".")[0]<39;function ne(t,e,n,i){return n=n||{},i?ie(t,e,n):ee&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):ie(t,e,n),n}function ie(t,e,n){if(r.domSupported&&t.getBoundingClientRect){var i=e.clientX,o=e.clientY;if(Jt(t)){var a=t.getBoundingClientRect();return n.zrX=i-a.left,void(n.zrY=o-a.top)}if($t(te,t,i,o))return n.zrX=te[0],void(n.zrY=te[1])}n.zrX=n.zrY=0}function re(t){return t||window.event}function oe(t,e,n){if(null!=(e=re(e)).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!==i?e.targetTouches[0]:e.changedTouches[0];r&&ne(t,r,e,n)}else{ne(t,e,e,n);var o=function(t){var e=t.wheelDelta;if(e)return e;var n=t.deltaX,i=t.deltaY;if(null==n||null==i)return e;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(e);e.zrDelta=o?o/120:-(e.detail||0)/3}var a=e.button;return null==e.which&&void 0!==a&&Qt.test(e.type)&&(e.which=1&a?1:2&a?3:4&a?2:0),e}function ae(t,e,n,i){t.addEventListener(e,n,i)}var se=function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0};function le(t){return 2===t.which||3===t.which}var ue=function(){function t(){this._track=[]}return t.prototype.recognize=function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},t.prototype.clear=function(){return this._track.length=0,this},t.prototype._doTrack=function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o1&&r&&r.length>1){var a=he(r)/he(o);!isFinite(a)&&(a=1),e.pinchScale=a;var s=[((i=r)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return e.pinchX=s[0],e.pinchY=s[1],{type:"pinch",target:t[0].target,event:e}}}}},pe="silent";function de(){se(this.event)}var fe=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.handler=null,e}return n(e,t),e.prototype.dispose=function(){},e.prototype.setCursor=function(){},e}(Xt),ge=function(t,e){this.x=t,this.y=e},ye=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],ve=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o._hovered=new ge(0,0),o.storage=e,o.painter=n,o.painterRoot=r,i=i||new fe,o.proxy=null,o.setHandlerProxy(i),o._draggingMgr=new Ut(o),o}return n(e,t),e.prototype.setHandlerProxy=function(t){this.proxy&&this.proxy.dispose(),t&&(E(ye,(function(e){t.on&&t.on(e,this[e],this)}),this),t.handler=this),this.proxy=t},e.prototype.mousemove=function(t){var e=t.zrX,n=t.zrY,i=xe(this,e,n),r=this._hovered,o=r.target;o&&!o.__zr&&(o=(r=this.findHover(r.x,r.y)).target);var a=this._hovered=i?new ge(e,n):this.findHover(e,n),s=a.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),o&&s!==o&&this.dispatchToElement(r,"mouseout",t),this.dispatchToElement(a,"mousemove",t),s&&s!==o&&this.dispatchToElement(a,"mouseover",t)},e.prototype.mouseout=function(t){var e=t.zrEventControl;"only_globalout"!==e&&this.dispatchToElement(this._hovered,"mouseout",t),"no_globalout"!==e&&this.trigger("globalout",{type:"globalout",event:t})},e.prototype.resize=function(){this._hovered=new ge(0,0)},e.prototype.dispatch=function(t,e){var n=this[t];n&&n.call(this,e)},e.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},e.prototype.setCursorStyle=function(t){var e=this.proxy;e.setCursor&&e.setCursor(t)},e.prototype.dispatchToElement=function(t,e,n){var i=(t=t||{}).target;if(!i||!i.silent){for(var r="on"+e,o=function(t,e,n){return{type:t,event:n,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:de}}(e,t,n);i&&(i[r]&&(o.cancelBubble=!!i[r].call(i,o)),i.trigger(e,o),i=i.__hostTarget?i.__hostTarget:i.parent,!o.cancelBubble););o.cancelBubble||(this.trigger(e,o),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(t){"function"==typeof t[r]&&t[r].call(t,o),t.trigger&&t.trigger(e,o)})))}},e.prototype.findHover=function(t,e,n){for(var i=this.storage.getDisplayList(),r=new ge(t,e),o=i.length-1;o>=0;o--){var a=void 0;if(i[o]!==n&&!i[o].ignore&&(a=me(i[o],t,e))&&(!r.topTarget&&(r.topTarget=i[o]),a!==pe)){r.target=i[o];break}}return r},e.prototype.processGesture=function(t,e){this._gestureMgr||(this._gestureMgr=new ue);var n=this._gestureMgr;"start"===e&&n.clear();var i=n.recognize(t,this.findHover(t.zrX,t.zrY,null).target,this.proxy.dom);if("end"===e&&n.clear(),i){var r=i.type;t.gestureEvent=r;var o=new ge;o.target=i.target,this.dispatchToElement(o,r,i.event)}},e}(Xt);function me(t,e,n){if(t[t.rectHover?"rectContain":"contain"](e,n)){for(var i=t,r=void 0,o=!1;i;){if(i.ignoreClip&&(o=!0),!o){var a=i.getClipPath();if(a&&!a.contain(e,n))return!1;i.silent&&(r=!0)}var s=i.__hostTarget;i=s||i.parent}return!r||pe}return!1}function xe(t,e,n){var i=t.painter;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}E(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(t){ve.prototype[t]=function(e){var n,i,r=e.zrX,o=e.zrY,a=xe(this,r,o);if("mouseup"===t&&a||(i=(n=this.findHover(r,o)).target),"mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Et(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}}));function _e(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r=0;)r++;return r-e}function be(t,e,n,i,r){for(i===e&&i++;i>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function we(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a>>1);o(t,e[n+h])>0?a=h+1:l=h}return l}function Se(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a>>1);o(t,e[n+h])<0?l=h:a=h+1}return l}function Me(t,e){var n,i,r=7,o=0;t.length;var a=[];function s(s){var l=n[s],u=i[s],h=n[s+1],c=i[s+1];i[s]=u+c,s===o-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),o--;var p=Se(t[h],t,l,u,0,e);l+=p,0!==(u-=p)&&0!==(c=we(t[l+u-1],t,h,c,c-1,e))&&(u<=c?function(n,i,o,s){var l=0;for(l=0;l=7||d>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l=0;l--)t[d+l]=t[p+l];return void(t[c]=a[h])}var f=r;for(;;){var g=0,y=0,v=!1;do{if(e(a[h],t[u])<0){if(t[c--]=t[u--],g++,y=0,0==--i){v=!0;break}}else if(t[c--]=a[h--],y++,g=0,1==--s){v=!0;break}}while((g|y)=0;l--)t[d+l]=t[p+l];if(0===i){v=!0;break}}if(t[c--]=a[h--],1==--s){v=!0;break}if(0!==(y=s-we(t[u],a,0,s,s-1,e))){for(s-=y,d=(c-=y)+1,p=(h-=y)+1,l=0;l=7||y>=7);if(v)break;f<0&&(f=0),f+=2}(r=f)<1&&(r=1);if(1===s){for(d=(c-=i)+1,p=(u-=i)+1,l=i-1;l>=0;l--)t[d+l]=t[p+l];t[c]=a[h]}else{if(0===s)throw new Error;for(p=c-(s-1),l=0;l1;){var t=o-2;if(t>=1&&i[t-1]<=i[t]+i[t+1]||t>=2&&i[t-2]<=i[t]+i[t-1])i[t-1]i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=_e(t,n,i,e))s&&(l=s),be(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}}var Te=!1;function Ce(){Te||(Te=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function De(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}var Ae=function(){function t(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=De}return t.prototype.traverse=function(t,e){for(var n=0;n0&&(u.__clipPaths=[]),isNaN(u.z)&&(Ce(),u.z=0),isNaN(u.z2)&&(Ce(),u.z2=0),isNaN(u.zlevel)&&(Ce(),u.zlevel=0),this._displayList[this._displayListLen++]=u}var h=t.getDecalElement&&t.getDecalElement();h&&this._updateAndAddDisplayable(h,e,n);var c=t.getTextGuideLine();c&&this._updateAndAddDisplayable(c,e,n);var p=t.getTextContent();p&&this._updateAndAddDisplayable(p,e,n)}},t.prototype.addRoot=function(t){t.__zr&&t.__zr.storage===this||this._roots.push(t)},t.prototype.delRoot=function(t){if(t instanceof Array)for(var e=0,n=t.length;e=0&&this._roots.splice(i,1)}},t.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},t.prototype.getRoots=function(){return this._roots},t.prototype.dispose=function(){this._displayList=null,this._roots=null},t}(),ke=r.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,16)},Le={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1,i=.4;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=i*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-Le.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*Le.bounceIn(2*t):.5*Le.bounceOut(2*t-1)+.5}},Pe=Math.pow,Oe=Math.sqrt,Re=1e-8,Ne=1e-4,Ee=Oe(3),ze=1/3,Ve=wt(),Be=wt(),Fe=wt();function Ge(t){return t>-1e-8&&tRe||t<-1e-8}function He(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function Ye(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function Ue(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,h=s*s-3*a*l,c=s*l-9*a*u,p=l*l-3*s*u,d=0;if(Ge(h)&&Ge(c)){if(Ge(s))o[0]=0;else(M=-l/s)>=0&&M<=1&&(o[d++]=M)}else{var f=c*c-4*h*p;if(Ge(f)){var g=c/h,y=-g/2;(M=-s/a+g)>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y)}else if(f>0){var v=Oe(f),m=h*s+1.5*a*(-c+v),x=h*s+1.5*a*(-c-v);(M=(-s-((m=m<0?-Pe(-m,ze):Pe(m,ze))+(x=x<0?-Pe(-x,ze):Pe(x,ze))))/(3*a))>=0&&M<=1&&(o[d++]=M)}else{var _=(2*h*s-3*a*c)/(2*Oe(h*h*h)),b=Math.acos(_)/3,w=Oe(h),S=Math.cos(b),M=(-s-2*w*S)/(3*a),I=(y=(-s+w*(S+Ee*Math.sin(b)))/(3*a),(-s+w*(S-Ee*Math.sin(b)))/(3*a));M>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y),I>=0&&I<=1&&(o[d++]=I)}}return d}function Xe(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(Ge(a)){if(We(o))(h=-s/o)>=0&&h<=1&&(r[l++]=h)}else{var u=o*o-4*a*s;if(Ge(u))r[0]=-o/(2*a);else if(u>0){var h,c=Oe(u),p=(-o-c)/(2*a);(h=(-o+c)/(2*a))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}function Ze(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,h=(l-s)*r+s,c=(h-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=c,o[4]=c,o[5]=h,o[6]=l,o[7]=i}function je(t,e,n,i,r,o,a,s,l,u,h){var c,p,d,f,g,y=.005,v=1/0;Ve[0]=l,Ve[1]=u;for(var m=0;m<1;m+=.05)Be[0]=He(t,n,r,a,m),Be[1]=He(e,i,o,s,m),(f=Vt(Ve,Be))=0&&f=0&&y=1?1:Ue(0,i,o,1,t,s)&&He(0,r,a,1,s[0])}}}var on=function(){function t(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||xt,this.ondestroy=t.ondestroy||xt,this.onrestart=t.onrestart||xt,t.easing&&this.setEasing(t.easing)}return t.prototype.step=function(t,e){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),!this._paused){var n=this._life,i=t-this._startTime-this._pausedTime,r=i/n;r<0&&(r=0),r=Math.min(r,1);var o=this.easingFunc,a=o?o(r):r;if(this.onframe(a),1===r){if(!this.loop)return!0;var s=i%n;this._startTime=t-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=e},t.prototype.pause=function(){this._paused=!0},t.prototype.resume=function(){this._paused=!1},t.prototype.setEasing=function(t){this.easing=t,this.easingFunc=U(t)?t:Le[t]||rn(t)},t}(),an=function(t){this.value=t},sn=function(){function t(){this._len=0}return t.prototype.insert=function(t){var e=new an(t);return this.insertEntry(e),e},t.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},t.prototype.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},t.prototype.len=function(){return this._len},t.prototype.clear=function(){this.head=this.tail=null,this._len=0},t}(),ln=function(){function t(t){this._list=new sn,this._maxSize=10,this._map={},this._maxSize=t}return t.prototype.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new an(e),a.key=t,n.insertEntry(a),i[t]=a}return r},t.prototype.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},t.prototype.clear=function(){this._list.clear(),this._map={}},t.prototype.len=function(){return this._list.len()},t}(),un={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function hn(t){return(t=Math.round(t))<0?0:t>255?255:t}function cn(t){return t<0?0:t>1?1:t}function pn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?hn(parseFloat(e)/100*255):hn(parseInt(e,10))}function dn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?cn(parseFloat(e)/100):cn(parseFloat(e))}function fn(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function gn(t,e,n){return t+(e-t)*n}function yn(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function vn(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}var mn=new ln(20),xn=null;function _n(t,e){xn&&vn(xn,e),xn=mn.put(t,xn||e.slice())}function bn(t,e){if(t){e=e||[];var n=mn.get(t);if(n)return vn(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in un)return vn(e,un[i]),_n(t,e),e;var r,o=i.length;if("#"===i.charAt(0))return 4===o||5===o?(r=parseInt(i.slice(1,4),16))>=0&&r<=4095?(yn(e,(3840&r)>>4|(3840&r)>>8,240&r|(240&r)>>4,15&r|(15&r)<<4,5===o?parseInt(i.slice(4),16)/15:1),_n(t,e),e):void yn(e,0,0,0,1):7===o||9===o?(r=parseInt(i.slice(1,7),16))>=0&&r<=16777215?(yn(e,(16711680&r)>>16,(65280&r)>>8,255&r,9===o?parseInt(i.slice(7),16)/255:1),_n(t,e),e):void yn(e,0,0,0,1):void 0;var a=i.indexOf("("),s=i.indexOf(")");if(-1!==a&&s+1===o){var l=i.substr(0,a),u=i.substr(a+1,s-(a+1)).split(","),h=1;switch(l){case"rgba":if(4!==u.length)return 3===u.length?yn(e,+u[0],+u[1],+u[2],1):yn(e,0,0,0,1);h=dn(u.pop());case"rgb":return 3!==u.length?void yn(e,0,0,0,1):(yn(e,pn(u[0]),pn(u[1]),pn(u[2]),h),_n(t,e),e);case"hsla":return 4!==u.length?void yn(e,0,0,0,1):(u[3]=dn(u[3]),wn(u,e),_n(t,e),e);case"hsl":return 3!==u.length?void yn(e,0,0,0,1):(wn(u,e),_n(t,e),e);default:return}}yn(e,0,0,0,1)}}function wn(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=dn(t[1]),r=dn(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return yn(e=e||[],hn(255*fn(a,o,n+1/3)),hn(255*fn(a,o,n)),hn(255*fn(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function Sn(t,e){var n=bn(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return kn(n,4===n.length?"rgba":"rgb")}}function Mn(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=hn(gn(a[0],s[0],l)),n[1]=hn(gn(a[1],s[1],l)),n[2]=hn(gn(a[2],s[2],l)),n[3]=cn(gn(a[3],s[3],l)),n}}var In=Mn;function Tn(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=bn(e[r]),s=bn(e[o]),l=i-r,u=kn([hn(gn(a[0],s[0],l)),hn(gn(a[1],s[1],l)),hn(gn(a[2],s[2],l)),cn(gn(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}var Cn=Tn;function Dn(t,e,n,i){var r=bn(t);if(t)return r=function(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var h=((s-i)/6+l/2)/l,c=((s-r)/6+l/2)/l,p=((s-o)/6+l/2)/l;i===s?e=p-c:r===s?e=1/3+h-p:o===s&&(e=2/3+c-h),e<0&&(e+=1),e>1&&(e-=1)}var d=[360*e,n,u];return null!=t[3]&&d.push(t[3]),d}}(r),null!=e&&(r[0]=function(t){return(t=Math.round(t))<0?0:t>360?360:t}(e)),null!=n&&(r[1]=dn(n)),null!=i&&(r[2]=dn(i)),kn(wn(r),"rgba")}function An(t,e){var n=bn(t);if(n&&null!=e)return n[3]=cn(e),kn(n,"rgba")}function kn(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function Ln(t,e){var n=bn(t);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*e:0}var Pn=Object.freeze({__proto__:null,parse:bn,lift:Sn,toHex:function(t){var e=bn(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)},fastLerp:Mn,fastMapToColor:In,lerp:Tn,mapToColor:Cn,modifyHSL:Dn,modifyAlpha:An,stringify:kn,lum:Ln,random:function(){return kn([Math.round(255*Math.random()),Math.round(255*Math.random()),Math.round(255*Math.random())],"rgb")}}),On=Math.round;function Rn(t){var e;if(t&&"transparent"!==t){if("string"==typeof t&&t.indexOf("rgba")>-1){var n=bn(t);n&&(t="rgb("+n[0]+","+n[1]+","+n[2]+")",e=n[3])}}else t="none";return{color:t,opacity:null==e?1:e}}var Nn=1e-4;function En(t){return t-1e-4}function zn(t){return On(1e3*t)/1e3}function Vn(t){return On(1e4*t)/1e4}var Bn={left:"start",right:"end",center:"middle",middle:"middle"};function Fn(t){return t&&!!t.image}function Gn(t){return"linear"===t.type}function Wn(t){return"radial"===t.type}function Hn(t){return"url(#"+t+")"}function Yn(t){var e=t.getGlobalScale(),n=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function Un(t){var e=t.x||0,n=t.y||0,i=(t.rotation||0)*_t,r=rt(t.scaleX,1),o=rt(t.scaleY,1),a=t.skewX||0,s=t.skewY||0,l=[];return(e||n)&&l.push("translate("+e+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===r&&1===o||l.push("scale("+r+","+o+")"),(a||s)&&l.push("skew("+On(a*_t)+"deg, "+On(s*_t)+"deg)"),l.join(" ")}var Xn=r.hasGlobalWindow&&U(window.btoa)?function(t){return window.btoa(unescape(t))}:"undefined"!=typeof Buffer?function(t){return Buffer.from(t).toString("base64")}:function(t){return null},Zn=Array.prototype.slice;function jn(t,e,n){return(e-t)*n+t}function qn(t,e,n,i){for(var r=e.length,o=0;oi?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;sa)i.length=a;else for(var s=o;s=1},t.prototype.getAdditiveTrack=function(){return this._additiveTrack},t.prototype.addKeyframe=function(t,e,n){this._needsSort=!0;var i=this.keyframes,r=i.length,o=!1,a=6,s=e;if(N(e)){var l=function(t){return N(t&&t[0])?2:1}(e);a=l,(1===l&&!j(e[0])||2===l&&!j(e[0][0]))&&(o=!0)}else if(j(e)&&!nt(e))a=0;else if(X(e))if(isNaN(+e)){var u=bn(e);u&&(s=u,a=3)}else a=0;else if(Q(e)){var h=A({},s);h.colorStops=z(e.colorStops,(function(t){return{offset:t.offset,color:bn(t.color)}})),Gn(e)?a=4:Wn(e)&&(a=5),s=h}0===r?this.valType=a:a===this.valType&&6!==a||(o=!0),this.discrete=this.discrete||o;var c={time:t,value:s,rawValue:e,percent:0};return n&&(c.easing=n,c.easingFunc=U(n)?n:Le[n]||rn(n)),i.push(c),c},t.prototype.prepare=function(t,e){var n=this.keyframes;this._needsSort&&n.sort((function(t,e){return t.time-e.time}));for(var i=this.valType,r=n.length,o=n[r-1],a=this.discrete,s=ii(i),l=ni(i),u=0;u=0&&!(l[n].percent<=e);n--);n=d(n,u-2)}else{for(n=p;ne);n++);n=d(n-1,u-2)}r=l[n+1],i=l[n]}if(i&&r){this._lastFr=n,this._lastFrP=e;var f=r.percent-i.percent,g=0===f?1:d((e-i.percent)/f,1);r.easingFunc&&(g=r.easingFunc(g));var y=o?this._additiveValue:c?ri:t[h];if(!ii(s)&&!c||y||(y=this._additiveValue=[]),this.discrete)t[h]=g<1?i.rawValue:r.rawValue;else if(ii(s))1===s?qn(y,i[a],r[a],g):function(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a0&&s.addKeyframe(0,ti(l),i),this._trackKeys.push(a)}s.addKeyframe(t,ti(e[a]),i)}return this._maxTime=Math.max(this._maxTime,t),this},t.prototype.pause=function(){this._clip.pause(),this._paused=!0},t.prototype.resume=function(){this._clip.resume(),this._paused=!1},t.prototype.isPaused=function(){return!!this._paused},t.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},t.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var e=t.length,n=0;n0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r1){var a=o.pop();r.addKeyframe(a.time,t[i]),r.prepare(this._maxTime,r.getAdditiveTrack())}}}},t}();function si(){return(new Date).getTime()}var li,ui,hi=function(t){function e(e){var n=t.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,e=e||{},n.stage=e.stage||{},n}return n(e,t),e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var e=t.getClip();e&&this.addClip(e)},e.prototype.removeClip=function(t){if(t.animation){var e=t.prev,n=t.next;e?e.next=n:this._head=n,n?n.prev=e:this._tail=e,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var e=t.getClip();e&&this.removeClip(e),t.animation=null},e.prototype.update=function(t){for(var e=si()-this._pausedTime,n=e-this._time,i=this._head;i;){var r=i.next;i.step(e,n)?(i.ondestroy(),this.removeClip(i),i=r):i=r}this._time=e,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0,ke((function e(){t._running&&(ke(e),!t._paused&&t.update())}))},e.prototype.start=function(){this._running||(this._time=si(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=si(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=si()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var e=t.next;t.prev=t.next=t.animation=null,t=e}this._head=this._tail=null},e.prototype.isFinished=function(){return null==this._head},e.prototype.animate=function(t,e){e=e||{},this.start();var n=new ai(t,e.loop);return this.addAnimator(n),n},e}(Xt),ci=r.domSupported,pi=(ui={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:li=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:z(li,(function(t){var e=t.replace("mouse","pointer");return ui.hasOwnProperty(e)?e:t}))}),di=["mousemove","mouseup"],fi=["pointermove","pointerup"],gi=!1;function yi(t){var e=t.pointerType;return"pen"===e||"touch"===e}function vi(t){t&&(t.zrByTouch=!0)}function mi(t,e){for(var n=e,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==e&&n===t.painterRoot);)n=n.parentNode;return i}var xi=function(t,e){this.stopPropagation=xt,this.stopImmediatePropagation=xt,this.preventDefault=xt,this.type=e.type,this.target=this.currentTarget=t.dom,this.pointerType=e.pointerType,this.clientX=e.clientX,this.clientY=e.clientY},_i={mousedown:function(t){t=oe(this.dom,t),this.__mayPointerCapture=[t.zrX,t.zrY],this.trigger("mousedown",t)},mousemove:function(t){t=oe(this.dom,t);var e=this.__mayPointerCapture;!e||t.zrX===e[0]&&t.zrY===e[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",t)},mouseup:function(t){t=oe(this.dom,t),this.__togglePointerCapture(!1),this.trigger("mouseup",t)},mouseout:function(t){mi(this,(t=oe(this.dom,t)).toElement||t.relatedTarget)||(this.__pointerCapturing&&(t.zrEventControl="no_globalout"),this.trigger("mouseout",t))},wheel:function(t){gi=!0,t=oe(this.dom,t),this.trigger("mousewheel",t)},mousewheel:function(t){gi||(t=oe(this.dom,t),this.trigger("mousewheel",t))},touchstart:function(t){vi(t=oe(this.dom,t)),this.__lastTouchMoment=new Date,this.handler.processGesture(t,"start"),_i.mousemove.call(this,t),_i.mousedown.call(this,t)},touchmove:function(t){vi(t=oe(this.dom,t)),this.handler.processGesture(t,"change"),_i.mousemove.call(this,t)},touchend:function(t){vi(t=oe(this.dom,t)),this.handler.processGesture(t,"end"),_i.mouseup.call(this,t),+new Date-+this.__lastTouchMoment<300&&_i.click.call(this,t)},pointerdown:function(t){_i.mousedown.call(this,t)},pointermove:function(t){yi(t)||_i.mousemove.call(this,t)},pointerup:function(t){_i.mouseup.call(this,t)},pointerout:function(t){yi(t)||_i.mouseout.call(this,t)}};E(["click","dblclick","contextmenu"],(function(t){_i[t]=function(e){e=oe(this.dom,e),this.trigger(t,e)}}));var bi={pointermove:function(t){yi(t)||bi.mousemove.call(this,t)},pointerup:function(t){bi.mouseup.call(this,t)},mousemove:function(t){this.trigger("mousemove",t)},mouseup:function(t){var e=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",t),e&&(t.zrEventControl="only_globalout",this.trigger("mouseout",t))}};function wi(t,e){var n=e.domHandlers;r.pointerEventsSupported?E(pi.pointer,(function(i){Mi(e,i,(function(e){n[i].call(t,e)}))})):(r.touchEventsSupported&&E(pi.touch,(function(i){Mi(e,i,(function(r){n[i].call(t,r),function(t){t.touching=!0,null!=t.touchTimer&&(clearTimeout(t.touchTimer),t.touchTimer=null),t.touchTimer=setTimeout((function(){t.touching=!1,t.touchTimer=null}),700)}(e)}))})),E(pi.mouse,(function(i){Mi(e,i,(function(r){r=re(r),e.touching||n[i].call(t,r)}))})))}function Si(t,e){function n(n){Mi(e,n,(function(i){i=re(i),mi(t,i.target)||(i=function(t,e){return oe(t.dom,new xi(t,e),!0)}(t,i),e.domHandlers[n].call(t,i))}),{capture:!0})}r.pointerEventsSupported?E(fi,n):r.touchEventsSupported||E(di,n)}function Mi(t,e,n,i){t.mounted[e]=n,t.listenerOpts[e]=i,ae(t.domTarget,e,n,i)}function Ii(t){var e,n,i,r,o=t.mounted;for(var a in o)o.hasOwnProperty(a)&&(e=t.domTarget,n=a,i=o[a],r=t.listenerOpts[a],e.removeEventListener(n,i,r));t.mounted={}}var Ti=function(t,e){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=t,this.domHandlers=e},Ci=function(t){function e(e,n){var i=t.call(this)||this;return i.__pointerCapturing=!1,i.dom=e,i.painterRoot=n,i._localHandlerScope=new Ti(e,_i),ci&&(i._globalHandlerScope=new Ti(document,bi)),wi(i,i._localHandlerScope),i}return n(e,t),e.prototype.dispose=function(){Ii(this._localHandlerScope),ci&&Ii(this._globalHandlerScope)},e.prototype.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},e.prototype.__togglePointerCapture=function(t){if(this.__mayPointerCapture=null,ci&&+this.__pointerCapturing^+t){this.__pointerCapturing=t;var e=this._globalHandlerScope;t?Si(this,e):Ii(e)}},e}(Xt),Di=1;r.hasGlobalWindow&&(Di=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var Ai=Di,ki="#333",Li="#ccc";function Pi(){return[1,0,0,1,0,0]}function Oi(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function Ri(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function Ni(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function Ei(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function zi(t,e,n){var i=e[0],r=e[2],o=e[4],a=e[1],s=e[3],l=e[5],u=Math.sin(n),h=Math.cos(n);return t[0]=i*h+a*u,t[1]=-i*u+a*h,t[2]=r*h+s*u,t[3]=-r*u+h*s,t[4]=h*o+u*l,t[5]=h*l-u*o,t}function Vi(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function Bi(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function Fi(t){var e=[1,0,0,1,0,0];return Ri(e,t),e}var Gi=Object.freeze({__proto__:null,create:Pi,identity:Oi,copy:Ri,mul:Ni,translate:Ei,rotate:zi,scale:Vi,invert:Bi,clone:Fi}),Wi=Oi,Hi=5e-5;function Yi(t){return t>Hi||t<-5e-5}var Ui=[],Xi=[],Zi=[1,0,0,1,0,0],ji=Math.abs,qi=function(){function t(){}return t.prototype.getLocalTransform=function(e){return t.getLocalTransform(this,e)},t.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},t.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},t.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},t.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},t.prototype.needLocalTransform=function(){return Yi(this.rotation)||Yi(this.x)||Yi(this.y)||Yi(this.scaleX-1)||Yi(this.scaleY-1)||Yi(this.skewX)||Yi(this.skewY)},t.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,e=this.needLocalTransform(),n=this.transform;e||t?(n=n||[1,0,0,1,0,0],e?this.getLocalTransform(n):Wi(n),t&&(e?Ni(n,t,n):Ri(n,t)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&Wi(n)},t.prototype._resolveGlobalScaleRatio=function(t){var e=this.globalScaleRatio;if(null!=e&&1!==e){this.getGlobalScale(Ui);var n=Ui[0]<0?-1:1,i=Ui[1]<0?-1:1,r=((Ui[0]-n)*e+n)/Ui[0]||0,o=((Ui[1]-i)*e+i)/Ui[1]||0;t[0]*=r,t[1]*=r,t[2]*=o,t[3]*=o}this.invTransform=this.invTransform||[1,0,0,1,0,0],Bi(this.invTransform,t)},t.prototype.getComputedTransform=function(){for(var t=this,e=[];t;)e.push(t),t=t.parent;for(;t=e.pop();)t.updateTransform();return this.transform},t.prototype.setLocalTransform=function(t){if(t){var e=t[0]*t[0]+t[1]*t[1],n=t[2]*t[2]+t[3]*t[3],i=Math.atan2(t[1],t[0]),r=Math.PI/2+i-Math.atan2(t[3],t[2]);n=Math.sqrt(n)*Math.cos(r),e=Math.sqrt(e),this.skewX=r,this.skewY=0,this.rotation=-i,this.x=+t[4],this.y=+t[5],this.scaleX=e,this.scaleY=n,this.originX=0,this.originY=0}},t.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(Ni(Xi,t.invTransform,e),e=Xi);var n=this.originX,i=this.originY;(n||i)&&(Zi[4]=n,Zi[5]=i,Ni(Xi,e,Zi),Xi[4]-=n,Xi[5]-=i,e=Xi),this.setLocalTransform(e)}},t.prototype.getGlobalScale=function(t){var e=this.transform;return t=t||[],e?(t[0]=Math.sqrt(e[0]*e[0]+e[1]*e[1]),t[1]=Math.sqrt(e[2]*e[2]+e[3]*e[3]),e[0]<0&&(t[0]=-t[0]),e[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},t.prototype.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&Ft(n,n,i),n},t.prototype.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&Ft(n,n,i),n},t.prototype.getLineScale=function(){var t=this.transform;return t&&ji(t[0]-1)>1e-10&&ji(t[3]-1)>1e-10?Math.sqrt(ji(t[0]*t[3]-t[2]*t[1])):1},t.prototype.copyTransform=function(t){$i(this,t)},t.getLocalTransform=function(t,e){e=e||[];var n=t.originX||0,i=t.originY||0,r=t.scaleX,o=t.scaleY,a=t.anchorX,s=t.anchorY,l=t.rotation||0,u=t.x,h=t.y,c=t.skewX?Math.tan(t.skewX):0,p=t.skewY?Math.tan(-t.skewY):0;if(n||i||a||s){var d=n+a,f=i+s;e[4]=-d*r-c*f*o,e[5]=-f*o-p*d*r}else e[4]=e[5]=0;return e[0]=r,e[3]=o,e[1]=p*r,e[2]=c*o,l&&zi(e,e,l),e[4]+=n+u,e[5]+=i+h,e},t.initDefaultProps=function(){var e=t.prototype;e.scaleX=e.scaleY=e.globalScaleRatio=1,e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0}(),t}(),Ki=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function $i(t,e){for(var n=0;nf&&(f=x,gf&&(f=_,v=n.x&&t<=n.x+n.width&&e>=n.y&&e<=n.y+n.height},t.prototype.clone=function(){return new t(this.x,this.y,this.width,this.height)},t.prototype.copy=function(e){t.copy(this,e)},t.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},t.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},t.prototype.isZero=function(){return 0===this.width||0===this.height},t.create=function(e){return new t(e.x,e.y,e.width,e.height)},t.copy=function(t,e){t.x=e.x,t.y=e.y,t.width=e.width,t.height=e.height},t.applyTransform=function(e,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var r=i[0],o=i[3],a=i[4],s=i[5];return e.x=n.x*r+a,e.y=n.y*o+s,e.width=n.width*r,e.height=n.height*o,e.width<0&&(e.x+=e.width,e.width=-e.width),void(e.height<0&&(e.y+=e.height,e.height=-e.height))}er.x=ir.x=n.x,er.y=rr.y=n.y,nr.x=rr.x=n.x+n.width,nr.y=ir.y=n.y+n.height,er.transform(i),rr.transform(i),nr.transform(i),ir.transform(i),e.x=Qi(er.x,nr.x,ir.x,rr.x),e.y=Qi(er.y,nr.y,ir.y,rr.y);var l=tr(er.x,nr.x,ir.x,rr.x),u=tr(er.y,nr.y,ir.y,rr.y);e.width=l-e.x,e.height=u-e.y}else e!==n&&t.copy(e,n)},t}(),lr={};function ur(t,e){var n=lr[e=e||a];n||(n=lr[e]=new ln(500));var i=n.get(t);return null==i&&(i=h.measureText(t,e).width,n.put(t,i)),i}function hr(t,e,n,i){var r=ur(t,e),o=fr(e),a=pr(0,r,n),s=dr(0,o,i);return new sr(a,s,r,o)}function cr(t,e,n,i){var r=((t||"")+"").split("\n");if(1===r.length)return hr(r[0],e,n,i);for(var o=new sr(0,0,0,0),a=0;a=0?parseFloat(t)/100*e:parseFloat(t):t}function yr(t,e,n){var i=e.position||"inside",r=null!=e.distance?e.distance:5,o=n.height,a=n.width,s=o/2,l=n.x,u=n.y,h="left",c="top";if(i instanceof Array)l+=gr(i[0],n.width),u+=gr(i[1],n.height),h=null,c=null;else switch(i){case"left":l-=r,u+=s,h="right",c="middle";break;case"right":l+=r+a,u+=s,c="middle";break;case"top":l+=a/2,u-=r,h="center",c="bottom";break;case"bottom":l+=a/2,u+=o+r,h="center";break;case"inside":l+=a/2,u+=s,h="center",c="middle";break;case"insideLeft":l+=r,u+=s,c="middle";break;case"insideRight":l+=a-r,u+=s,h="right",c="middle";break;case"insideTop":l+=a/2,u+=r,h="center";break;case"insideBottom":l+=a/2,u+=o-r,h="center",c="bottom";break;case"insideTopLeft":l+=r,u+=r;break;case"insideTopRight":l+=a-r,u+=r,h="right";break;case"insideBottomLeft":l+=r,u+=o-r,c="bottom";break;case"insideBottomRight":l+=a-r,u+=o-r,h="right",c="bottom"}return(t=t||{}).x=l,t.y=u,t.align=h,t.verticalAlign=c,t}var vr="__zr_normal__",mr=Ki.concat(["ignore"]),xr=V(Ki,(function(t,e){return t[e]=!0,t}),{ignore:!1}),_r={},br=new sr(0,0,0,0),wr=function(){function t(t){this.id=M(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return t.prototype._init=function(t){this.attr(t)},t.prototype.drift=function(t,e,n){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=t,i[5]+=e,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(t){var e=this._textContent;if(e&&(!e.ignore||t)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,r=e.innerTransformable,o=void 0,a=void 0,s=!1;r.parent=i?this:null;var l=!1;if(r.copyTransform(e),null!=n.position){var u=br;n.layoutRect?u.copy(n.layoutRect):u.copy(this.getBoundingRect()),i||u.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(_r,n,u):yr(_r,n,u),r.x=_r.x,r.y=_r.y,o=_r.align,a=_r.verticalAlign;var h=n.origin;if(h&&null!=n.rotation){var c=void 0,p=void 0;"center"===h?(c=.5*u.width,p=.5*u.height):(c=gr(h[0],u.width),p=gr(h[1],u.height)),l=!0,r.originX=-r.x+c+(i?0:u.x),r.originY=-r.y+p+(i?0:u.y)}}null!=n.rotation&&(r.rotation=n.rotation);var d=n.offset;d&&(r.x+=d[0],r.y+=d[1],l||(r.originX=-d[0],r.originY=-d[1]));var f=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),y=void 0,v=void 0,m=void 0;f&&this.canBeInsideText()?(y=n.insideFill,v=n.insideStroke,null!=y&&"auto"!==y||(y=this.getInsideTextFill()),null!=v&&"auto"!==v||(v=this.getInsideTextStroke(y),m=!0)):(y=n.outsideFill,v=n.outsideStroke,null!=y&&"auto"!==y||(y=this.getOutsideFill()),null!=v&&"auto"!==v||(v=this.getOutsideStroke(y),m=!0)),(y=y||"#000")===g.fill&&v===g.stroke&&m===g.autoStroke&&o===g.align&&a===g.verticalAlign||(s=!0,g.fill=y,g.stroke=v,g.autoStroke=m,g.align=o,g.verticalAlign=a,e.setDefaultTextStyle(g)),e.__dirty|=1,s&&e.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(t){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?Li:ki},t.prototype.getOutsideStroke=function(t){var e=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof e&&bn(e);n||(n=[255,255,255,1]);for(var i=n[3],r=this.__zr.isDarkMode(),o=0;o<3;o++)n[o]=n[o]*i+(r?0:255)*(1-i);return n[3]=1,kn(n,"rgba")},t.prototype.traverse=function(t,e){},t.prototype.attrKV=function(t,e){"textConfig"===t?this.setTextConfig(e):"textContent"===t?this.setTextContent(e):"clipPath"===t?this.setClipPath(e):"extra"===t?(this.extra=this.extra||{},A(this.extra,e)):this[t]=e},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(t,e){if("string"==typeof t)this.attrKV(t,e);else if(q(t))for(var n=G(t),i=0;i0},t.prototype.getState=function(t){return this.states[t]},t.prototype.ensureState=function(t){var e=this.states;return e[t]||(e[t]={}),e[t]},t.prototype.clearStates=function(t){this.useState(vr,!1,t)},t.prototype.useState=function(t,e,n,i){var r=t===vr;if(this.hasState()||!r){var o=this.currentStates,a=this.stateTransition;if(!(P(o,t)>=0)||!e&&1!==o.length){var s;if(this.stateProxy&&!r&&(s=this.stateProxy(t)),s||(s=this.states&&this.states[t]),s||r){r||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(t,s,this._normalState,e,!n&&!this.__inHover&&a&&a.duration>0,a);var u=this._textContent,h=this._textGuide;return u&&u.useState(t,e,n,l),h&&h.useState(t,e,n,l),r?(this.currentStates=[],this._normalState={}):e?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}I("State "+t+" not exists.")}}},t.prototype.useStates=function(t,e,n){if(t.length){var i=[],r=this.currentStates,o=t.length,a=o===r.length;if(a)for(var s=0;s0,d);var f=this._textContent,g=this._textGuide;f&&f.useStates(t,e,c),g&&g.useStates(t,e,c),this._updateAnimationTargets(),this.currentStates=t.slice(),this.markRedraw(),!c&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},t.prototype._updateAnimationTargets=function(){for(var t=0;t=0){var n=this.currentStates.slice();n.splice(e,1),this.useStates(n)}},t.prototype.replaceState=function(t,e,n){var i=this.currentStates.slice(),r=P(i,t),o=P(i,e)>=0;r>=0?o?i.splice(r,1):i[r]=e:n&&!o&&i.push(e),this.useStates(i)},t.prototype.toggleState=function(t,e){e?this.useState(t,!0):this.removeState(t)},t.prototype._mergeStates=function(t){for(var e,n={},i=0;i=0&&e.splice(n,1)})),this.animators.push(t),n&&n.animation.addAnimator(t),n&&n.wakeUp()},t.prototype.updateDuringAnimation=function(t){this.markRedraw()},t.prototype.stopAnimation=function(t,e){for(var n=this.animators,i=n.length,r=[],o=0;o0&&n.during&&o[0].during((function(t,e){n.during(e)}));for(var p=0;p0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},p&&(S={});for(_=0;_=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,e){var n=P(this._children,t);return n>=0&&this.replaceAt(e,n),this},e.prototype.replaceAt=function(t,e){var n=this._children,i=n[e];if(t&&t!==this&&t.parent!==this&&t!==i){n[e]=t,i.parent=null;var r=this.__zr;r&&i.removeSelfFromZr(r),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__zr;e&&e!==t.__zr&&t.addSelfToZr(e),e&&e.refresh()},e.prototype.remove=function(t){var e=this.__zr,n=this._children,i=P(n,t);return i<0||(n.splice(i,1),t.parent=null,e&&t.removeSelfFromZr(e),e&&e.refresh()),this},e.prototype.removeAll=function(){for(var t=this._children,e=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this.animation.start(),this._stillFrameAccum=0},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover()},t.prototype.resize=function(t){t=t||{},this.painter.resize(t.width,t.height),this.handler.resize()},t.prototype.clearAnimation=function(){this.animation.clear()},t.prototype.getWidth=function(){return this.painter.getWidth()},t.prototype.getHeight=function(){return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this.handler.off(t,e)},t.prototype.trigger=function(t,e){this.handler.trigger(t,e)},t.prototype.clear=function(){for(var t=this.storage.getRoots(),e=0;e0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}function Er(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return X(t)?(n=t,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t;var n}function zr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function Vr(t){return t.sort((function(t,e){return t-e})),t}function Br(t){if(t=+t,isNaN(t))return 0;if(t>1e-14)for(var e=1,n=0;n<15;n++,e*=10)if(Math.round(t*e)/e===t)return n;return Fr(t)}function Fr(t){var e=t.toString().toLowerCase(),n=e.indexOf("e"),i=n>0?+e.slice(n+1):0,r=n>0?n:e.length,o=e.indexOf("."),a=o<0?0:r-1-o;return Math.max(0,a-i)}function Gr(t,e){var n=Math.log,i=Math.LN10,r=Math.floor(n(t[1]-t[0])/i),o=Math.round(n(Math.abs(e[1]-e[0]))/i),a=Math.min(Math.max(-r+o,0),20);return isFinite(a)?a:20}function Wr(t,e,n){if(!t[e])return 0;var i=V(t,(function(t,e){return t+(isNaN(e)?0:e)}),0);if(0===i)return 0;for(var r=Math.pow(10,n),o=z(t,(function(t){return(isNaN(t)?0:t)/i*r*100})),a=100*r,s=z(o,(function(t){return Math.floor(t)})),l=V(s,(function(t,e){return t+e}),0),u=z(o,(function(t,e){return t-s[e]}));lh&&(h=u[p],c=p);++s[c],u[c]=0,++l}return s[e]/r}function Hr(t,e){var n=Math.max(Br(t),Br(e)),i=t+e;return n>20?i:zr(i,n)}var Yr=9007199254740991;function Ur(t){var e=2*Math.PI;return(t%e+e)%e}function Xr(t){return t>-1e-4&&t=10&&e++,e}function $r(t,e){var n=Kr(t),i=Math.pow(10,n),r=t/i;return t=(e?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10)*i,n>=-20?+t.toFixed(n<0?-n:0):t}function Jr(t,e){var n=(t.length-1)*e+1,i=Math.floor(n),r=+t[i-1],o=n-i;return o?r+o*(t[i]-r):r}function Qr(t){t.sort((function(t,e){return s(t,e,0)?-1:1}));for(var e=-1/0,n=1,i=0;i=0||r&&P(r,s)<0)){var l=n.getShallow(s,e);null!=l&&(o[t[a][0]]=l)}}return o}}var Ho=Wo([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),Yo=function(){function t(){}return t.prototype.getAreaStyle=function(t,e){return Ho(this,t,e)},t}(),Uo=new ln(50);function Xo(t){if("string"==typeof t){var e=Uo.get(t);return e&&e.image}return t}function Zo(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=Uo.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!qo(e=o.image)&&o.pending.push(a):((e=h.loadImage(t,jo,jo)).__zrImageSrc=t,Uo.put(t,e.__cachedImgObj={image:e,pending:[a]})),e}return t}return e}function jo(){var t=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=a;l++)s-=a;var u=ur(n,e);return u>s&&(n="",u=0),s=t-u,r.ellipsis=n,r.ellipsisWidth=u,r.contentWidth=s,r.containerWidth=t,r}function Qo(t,e){var n=e.containerWidth,i=e.font,r=e.contentWidth;if(!n)return"";var o=ur(t,i);if(o<=n)return t;for(var a=0;;a++){if(o<=r||a>=e.maxIterations){t+=e.ellipsis;break}var s=0===a?ta(t,r,e.ascCharWidth,e.cnCharWidth):o>0?Math.floor(t.length*r/o):0;o=ur(t=t.substr(0,s),i)}return""===t&&(t=e.placeholder),t}function ta(t,e,n,i){for(var r=0,o=0,a=t.length;o0&&f+i.accumWidth>i.width&&(o=e.split("\n"),c=!0),i.accumWidth=f}else{var g=sa(e,h,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+d,a=g.linesWidths,o=g.lines}}else o=e.split("\n");for(var y=0;y=33&&e<=383}(t)||!!oa[t]}function sa(t,e,n,i,r){for(var o=[],a=[],s="",l="",u=0,h=0,c=0;cn:r+h+d>n)?h?(s||l)&&(f?(s||(s=l,l="",h=u=0),o.push(s),a.push(h-u),l+=p,s="",h=u+=d):(l&&(s+=l,l="",u=0),o.push(s),a.push(h),s=p,h=d)):f?(o.push(l),a.push(u),l=p,u=d):(o.push(p),a.push(d)):(h+=d,f?(l+=p,u+=d):(l&&(s+=l,l="",u=0),s+=p))}else l&&(s+=l,h+=u),o.push(s),a.push(h),s="",l="",u=0,h=0}return o.length||s||(s=t,l="",u=0),l&&(s+=l),s&&(o.push(s),a.push(h)),1===o.length&&(h+=r),{accumWidth:h,lines:o,linesWidths:a}}var la="__zr_style_"+Math.round(10*Math.random()),ua={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},ha={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};ua[la]=!0;var ca=["z","z2","invisible"],pa=["invisible"],da=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype._init=function(e){for(var n=G(e),i=0;i1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(ba[0]=xa(r)*n+t,ba[1]=ma(r)*i+e,wa[0]=xa(o)*n+t,wa[1]=ma(o)*i+e,u(s,ba,wa),h(l,ba,wa),(r%=_a)<0&&(r+=_a),(o%=_a)<0&&(o+=_a),r>o&&!a?o+=_a:rr&&(Sa[0]=xa(d)*n+t,Sa[1]=ma(d)*i+e,u(s,Sa,s),h(l,Sa,l))}var La={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Pa=[],Oa=[],Ra=[],Na=[],Ea=[],za=[],Va=Math.min,Ba=Math.max,Fa=Math.cos,Ga=Math.sin,Wa=Math.abs,Ha=Math.PI,Ya=2*Ha,Ua="undefined"!=typeof Float32Array,Xa=[];function Za(t){return Math.round(t/Ha*1e8)/1e8%2*Ha}function ja(t,e){var n=Za(t[0]);n<0&&(n+=Ya);var i=n-t[0],r=t[1];r+=i,!e&&r-n>=Ya?r=n+Ya:e&&n-r>=Ya?r=n-Ya:!e&&n>r?r=n+(Ya-Za(n-r)):e&&n0&&(this._ux=Wa(n/Ai/t)||0,this._uy=Wa(n/Ai/e)||0)},t.prototype.setDPR=function(t){this.dpr=t},t.prototype.setContext=function(t){this._ctx=t},t.prototype.getContext=function(){return this._ctx},t.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},t.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},t.prototype.moveTo=function(t,e){return this._drawPendingPt(),this.addData(La.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},t.prototype.lineTo=function(t,e){var n=Wa(t-this._xi),i=Wa(e-this._yi),r=n>this._ux||i>this._uy;if(this.addData(La.L,t,e),this._ctx&&r&&this._ctx.lineTo(t,e),r)this._xi=t,this._yi=e,this._pendingPtDist=0;else{var o=n*n+i*i;o>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=e,this._pendingPtDist=o)}return this},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){return this._drawPendingPt(),this.addData(La.C,t,e,n,i,r,o),this._ctx&&this._ctx.bezierCurveTo(t,e,n,i,r,o),this._xi=r,this._yi=o,this},t.prototype.quadraticCurveTo=function(t,e,n,i){return this._drawPendingPt(),this.addData(La.Q,t,e,n,i),this._ctx&&this._ctx.quadraticCurveTo(t,e,n,i),this._xi=n,this._yi=i,this},t.prototype.arc=function(t,e,n,i,r,o){this._drawPendingPt(),Xa[0]=i,Xa[1]=r,ja(Xa,o),i=Xa[0];var a=(r=Xa[1])-i;return this.addData(La.A,t,e,n,n,i,a,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=Fa(r)*n+t,this._yi=Ga(r)*n+e,this},t.prototype.arcTo=function(t,e,n,i,r){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},t.prototype.rect=function(t,e,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,e,n,i),this.addData(La.R,t,e,n,i),this},t.prototype.closePath=function(){this._drawPendingPt(),this.addData(La.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&t.closePath(),this._xi=e,this._yi=n,this},t.prototype.fill=function(t){t&&t.fill(),this.toStatic()},t.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},t.prototype.len=function(){return this._len},t.prototype.setData=function(t){var e=t.length;this.data&&this.data.length===e||!Ua||(this.data=new Float32Array(e));for(var n=0;nu.length&&(this._expandData(),u=this.data);for(var h=0;h0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},t.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e11&&(this.data=new Float32Array(t)))}},t.prototype.getBoundingRect=function(){Ra[0]=Ra[1]=Ea[0]=Ea[1]=Number.MAX_VALUE,Na[0]=Na[1]=za[0]=za[1]=-Number.MAX_VALUE;var t,e=this.data,n=0,i=0,r=0,o=0;for(t=0;tn||Wa(y)>i||c===e-1)&&(f=Math.sqrt(A*A+y*y),r=g,o=x);break;case La.C:var v=t[c++],m=t[c++],x=(g=t[c++],t[c++]),_=t[c++],b=t[c++];f=qe(r,o,v,m,g,x,_,b,10),r=_,o=b;break;case La.Q:f=en(r,o,v=t[c++],m=t[c++],g=t[c++],x=t[c++],10),r=g,o=x;break;case La.A:var w=t[c++],S=t[c++],M=t[c++],I=t[c++],T=t[c++],C=t[c++],D=C+T;c+=1;t[c++];d&&(a=Fa(T)*M+w,s=Ga(T)*I+S),f=Ba(M,I)*Va(Ya,Math.abs(C)),r=Fa(D)*M+w,o=Ga(D)*I+S;break;case La.R:a=r=t[c++],s=o=t[c++],f=2*t[c++]+2*t[c++];break;case La.Z:var A=a-r;y=s-o;f=Math.sqrt(A*A+y*y),r=a,o=s}f>=0&&(l[h++]=f,u+=f)}return this._pathLen=u,u},t.prototype.rebuildPath=function(t,e){var n,i,r,o,a,s,l,u,h,c,p=this.data,d=this._ux,f=this._uy,g=this._len,y=e<1,v=0,m=0,x=0;if(!y||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,u=e*this._pathLen))t:for(var _=0;_0&&(t.lineTo(h,c),x=0),b){case La.M:n=r=p[_++],i=o=p[_++],t.moveTo(r,o);break;case La.L:a=p[_++],s=p[_++];var S=Wa(a-r),M=Wa(s-o);if(S>d||M>f){if(y){if(v+(j=l[m++])>u){var I=(u-v)/j;t.lineTo(r*(1-I)+a*I,o*(1-I)+s*I);break t}v+=j}t.lineTo(a,s),r=a,o=s,x=0}else{var T=S*S+M*M;T>x&&(h=a,c=s,x=T)}break;case La.C:var C=p[_++],D=p[_++],A=p[_++],k=p[_++],L=p[_++],P=p[_++];if(y){if(v+(j=l[m++])>u){Ze(r,C,A,L,I=(u-v)/j,Pa),Ze(o,D,k,P,I,Oa),t.bezierCurveTo(Pa[1],Oa[1],Pa[2],Oa[2],Pa[3],Oa[3]);break t}v+=j}t.bezierCurveTo(C,D,A,k,L,P),r=L,o=P;break;case La.Q:C=p[_++],D=p[_++],A=p[_++],k=p[_++];if(y){if(v+(j=l[m++])>u){Qe(r,C,A,I=(u-v)/j,Pa),Qe(o,D,k,I,Oa),t.quadraticCurveTo(Pa[1],Oa[1],Pa[2],Oa[2]);break t}v+=j}t.quadraticCurveTo(C,D,A,k),r=A,o=k;break;case La.A:var O=p[_++],R=p[_++],N=p[_++],E=p[_++],z=p[_++],V=p[_++],B=p[_++],F=!p[_++],G=N>E?N:E,W=Wa(N-E)>.001,H=z+V,Y=!1;if(y)v+(j=l[m++])>u&&(H=z+V*(u-v)/j,Y=!0),v+=j;if(W&&t.ellipse?t.ellipse(O,R,N,E,B,z,H,F):t.arc(O,R,G,z,H,F),Y)break t;w&&(n=Fa(z)*N+O,i=Ga(z)*E+R),r=Fa(H)*N+O,o=Ga(H)*E+R;break;case La.R:n=r=p[_],i=o=p[_+1],a=p[_++],s=p[_++];var U=p[_++],X=p[_++];if(y){if(v+(j=l[m++])>u){var Z=u-v;t.moveTo(a,s),t.lineTo(a+Va(Z,U),s),(Z-=U)>0&&t.lineTo(a+U,s+Va(Z,X)),(Z-=X)>0&&t.lineTo(a+Ba(U-Z,0),s+X),(Z-=U)>0&&t.lineTo(a,s+Ba(X-Z,0));break t}v+=j}t.rect(a,s,U,X);break;case La.Z:if(y){var j;if(v+(j=l[m++])>u){I=(u-v)/j;t.lineTo(r*(1-I)+n*I,o*(1-I)+i*I);break t}v+=j}t.closePath(),r=n,o=i}}},t.prototype.clone=function(){var e=new t,n=this.data;return e.data=n.slice?n.slice():Array.prototype.slice.call(n),e._len=this._len,e},t.CMD=La,t.initDefaultProps=function(){var e=t.prototype;e._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,e._version=0}(),t}();function Ka(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0;if(a>e+s&&a>i+s||at+s&&o>n+s||oe+c&&h>i+c&&h>o+c&&h>s+c||ht+c&&u>n+c&&u>r+c&&u>a+c||ue+u&&l>i+u&&l>o+u||lt+u&&s>n+u&&s>r+u||sn||h+ur&&(r+=es);var p=Math.atan2(l,s);return p<0&&(p+=es),p>=i&&p<=r||p+es>=i&&p+es<=r}function is(t,e,n,i,r,o){if(o>e&&o>i||or?s:0}var rs=qa.CMD,os=2*Math.PI;var as=[-1,-1,-1],ss=[-1,-1];function ls(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u1&&(h=void 0,h=ss[0],ss[0]=ss[1],ss[1]=h),f=He(e,i,o,s,ss[0]),d>1&&(g=He(e,i,o,s,ss[1]))),2===d?ve&&s>i&&s>o||s=0&&h<=1&&(r[l++]=h);else{var u=a*a-4*o*s;if(Ge(u))(h=-a/(2*o))>=0&&h<=1&&(r[l++]=h);else if(u>0){var h,c=Oe(u),p=(-a-c)/(2*o);(h=(-a+c)/(2*o))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}(e,i,o,s,as);if(0===l)return 0;var u=Je(e,i,o);if(u>=0&&u<=1){for(var h=0,c=Ke(e,i,o,u),p=0;pn||s<-n)return 0;var l=Math.sqrt(n*n-s*s);as[0]=-l,as[1]=l;var u=Math.abs(i-r);if(u<1e-4)return 0;if(u>=os-1e-4){i=0,r=os;var h=o?1:-1;return a>=as[0]+t&&a<=as[1]+t?h:0}if(i>r){var c=i;i=r,r=c}i<0&&(i+=os,r+=os);for(var p=0,d=0;d<2;d++){var f=as[d];if(f+t>a){var g=Math.atan2(s,f);h=o?1:-1;g<0&&(g=os+g),(g>=i&&g<=r||g+os>=i&&g+os<=r)&&(g>Math.PI/2&&g<1.5*Math.PI&&(h=-h),p+=h)}}return p}function cs(t,e,n,i,r){for(var o,a,s,l,u=t.data,h=t.len(),c=0,p=0,d=0,f=0,g=0,y=0;y1&&(n||(c+=is(p,d,f,g,i,r))),m&&(f=p=u[y],g=d=u[y+1]),v){case rs.M:p=f=u[y++],d=g=u[y++];break;case rs.L:if(n){if(Ka(p,d,u[y],u[y+1],e,i,r))return!0}else c+=is(p,d,u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case rs.C:if(n){if($a(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=ls(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case rs.Q:if(n){if(Ja(p,d,u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=us(p,d,u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case rs.A:var x=u[y++],_=u[y++],b=u[y++],w=u[y++],S=u[y++],M=u[y++];y+=1;var I=!!(1-u[y++]);o=Math.cos(S)*b+x,a=Math.sin(S)*w+_,m?(f=o,g=a):c+=is(p,d,o,a,i,r);var T=(i-x)*w/b+x;if(n){if(ns(x,_,w,S,S+M,I,e,T,r))return!0}else c+=hs(x,_,w,S,S+M,I,T,r);p=Math.cos(S+M)*b+x,d=Math.sin(S+M)*w+_;break;case rs.R:if(f=p=u[y++],g=d=u[y++],o=f+u[y++],a=g+u[y++],n){if(Ka(f,g,o,g,e,i,r)||Ka(o,g,o,a,e,i,r)||Ka(o,a,f,a,e,i,r)||Ka(f,a,f,g,e,i,r))return!0}else c+=is(o,g,o,a,i,r),c+=is(f,a,f,g,i,r);break;case rs.Z:if(n){if(Ka(p,d,f,g,e,i,r))return!0}else c+=is(p,d,f,g,i,r);p=f,d=g}}return n||(s=d,l=g,Math.abs(s-l)<1e-4)||(c+=is(p,d,f,g,i,r)||0),0!==c}var ps=k({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},ua),ds={style:k({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},ha.style)},fs=Ki.concat(["invisible","culling","z","z2","zlevel","parent"]),gs=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype.update=function(){var n=this;t.prototype.update.call(this);var i=this.style;if(i.decal){var r=this._decalEl=this._decalEl||new e;r.buildPath===e.prototype.buildPath&&(r.buildPath=function(t){n.buildPath(t,n.shape)}),r.silent=!0;var o=r.style;for(var a in i)o[a]!==i[a]&&(o[a]=i[a]);o.fill=i.fill?i.decal:null,o.decal=null,o.shadowColor=null,i.strokeFirst&&(o.stroke=null);for(var s=0;s.5?ki:e>.2?"#eee":Li}if(t)return Li}return ki},e.prototype.getInsideTextStroke=function(t){var e=this.style.fill;if(X(e)){var n=this.__zr;if(!(!n||!n.isDarkMode())===Ln(t,0)<.4)return e}},e.prototype.buildPath=function(t,e,n){},e.prototype.pathUpdated=function(){this.__dirty&=-5},e.prototype.getUpdatedPathProxy=function(t){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,t),this.path},e.prototype.createPathProxy=function(){this.path=new qa(!1)},e.prototype.hasStroke=function(){var t=this.style,e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.getBoundingRect=function(){var t=this._rect,e=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var r=this.path;(i||4&this.__dirty)&&(r.beginPath(),this.buildPath(r,this.shape,!1),this.pathUpdated()),t=r.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var o=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){o.copy(t);var a=e.strokeNoScale?this.getLineScale():1,s=e.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}a>1e-10&&(o.width+=s/a,o.height+=s/a,o.x-=s/a/2,o.y-=s/a/2)}return o}return t},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path;if(this.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),function(t,e,n,i){return cs(t,e,!0,n,i)}(o,a/s,t,e)))return!0}if(this.hasFill())return function(t,e,n){return cs(t,0,!1,e,n)}(o,t,e)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){"style"===t?this.dirtyStyle():"shape"===t?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(e,n){"shape"===e?this.setShape(n):t.prototype.attrKV.call(this,e,n)},e.prototype.setShape=function(t,e){var n=this.shape;return n||(n=this.shape={}),"string"==typeof t?n[t]=e:A(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(4&this.__dirty)},e.prototype.createStyle=function(t){return yt(ps,t)},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.shape&&!n.shape&&(n.shape=A({},this.shape))},e.prototype._applyStateObj=function(e,n,i,r,o,a){t.prototype._applyStateObj.call(this,e,n,i,r,o,a);var s,l=!(n&&r);if(n&&n.shape?o?r?s=n.shape:(s=A({},i.shape),A(s,n.shape)):(s=A({},r?this.shape:i.shape),A(s,n.shape)):l&&(s=i.shape),s)if(o){this.shape=A({},this.shape);for(var u={},h=G(s),c=0;c0},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.createStyle=function(t){return yt(ys,t)},e.prototype.setBoundingRect=function(t){this._rect=t},e.prototype.getBoundingRect=function(){var t=this.style;if(!this._rect){var e=t.text;null!=e?e+="":e="";var n=cr(e,t.font,t.textAlign,t.textBaseline);if(n.x+=t.x||0,n.y+=t.y||0,this.hasStroke()){var i=t.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},e.initDefaultProps=void(e.prototype.dirtyRectTolerance=10),e}(da);vs.prototype.type="tspan";var ms=k({x:0,y:0},ua),xs={style:k({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},ha.style)};var _s=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.createStyle=function(t){return yt(ms,t)},e.prototype._getSize=function(t){var e=this.style,n=e[t];if(null!=n)return n;var i,r=(i=e.image)&&"string"!=typeof i&&i.width&&i.height?e.image:this.__image;if(!r)return 0;var o="width"===t?"height":"width",a=e[o];return null==a?r[t]:r[t]/r[o]*a},e.prototype.getWidth=function(){return this._getSize("width")},e.prototype.getHeight=function(){return this._getSize("height")},e.prototype.getAnimationStyleProps=function(){return xs},e.prototype.getBoundingRect=function(){var t=this.style;return this._rect||(this._rect=new sr(t.x||0,t.y||0,this.getWidth(),this.getHeight())),this._rect},e}(da);_s.prototype.type="image";var bs=Math.round;function ws(t,e,n){if(e){var i=e.x1,r=e.x2,o=e.y1,a=e.y2;t.x1=i,t.x2=r,t.y1=o,t.y2=a;var s=n&&n.lineWidth;return s?(bs(2*i)===bs(2*r)&&(t.x1=t.x2=Ms(i,s,!0)),bs(2*o)===bs(2*a)&&(t.y1=t.y2=Ms(o,s,!0)),t):t}}function Ss(t,e,n){if(e){var i=e.x,r=e.y,o=e.width,a=e.height;t.x=i,t.y=r,t.width=o,t.height=a;var s=n&&n.lineWidth;return s?(t.x=Ms(i,s,!0),t.y=Ms(r,s,!0),t.width=Math.max(Ms(i+o,s,!1)-t.x,0===o?0:1),t.height=Math.max(Ms(r+a,s,!1)-t.y,0===a?0:1),t):t}}function Ms(t,e,n){if(!e)return t;var i=bs(2*t);return(i+bs(e))%2==0?i/2:(i+(n?1:-1))/2}var Is=function(){this.x=0,this.y=0,this.width=0,this.height=0},Ts={},Cs=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Is},e.prototype.buildPath=function(t,e){var n,i,r,o;if(this.subPixelOptimize){var a=Ss(Ts,e,this.style);n=a.x,i=a.y,r=a.width,o=a.height,a.r=e.r,e=a}else n=e.x,i=e.y,r=e.width,o=e.height;e.r?function(t,e){var n,i,r,o,a,s=e.x,l=e.y,u=e.width,h=e.height,c=e.r;u<0&&(s+=u,u=-u),h<0&&(l+=h,h=-h),"number"==typeof c?n=i=r=o=c:c instanceof Array?1===c.length?n=i=r=o=c[0]:2===c.length?(n=r=c[0],i=o=c[1]):3===c.length?(n=c[0],i=o=c[1],r=c[2]):(n=c[0],i=c[1],r=c[2],o=c[3]):n=i=r=o=0,n+i>u&&(n*=u/(a=n+i),i*=u/a),r+o>u&&(r*=u/(a=r+o),o*=u/a),i+r>h&&(i*=h/(a=i+r),r*=h/a),n+o>h&&(n*=h/(a=n+o),o*=h/a),t.moveTo(s+n,l),t.lineTo(s+u-i,l),0!==i&&t.arc(s+u-i,l+i,i,-Math.PI/2,0),t.lineTo(s+u,l+h-r),0!==r&&t.arc(s+u-r,l+h-r,r,0,Math.PI/2),t.lineTo(s+o,l+h),0!==o&&t.arc(s+o,l+h-o,o,Math.PI/2,Math.PI),t.lineTo(s,l+n),0!==n&&t.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(t,e):t.rect(n,i,r,o)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(gs);Cs.prototype.type="rect";var Ds={fill:"#000"},As={style:k({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},ha.style)},ks=function(t){function e(e){var n=t.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Ds,n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){t.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var e=0;ed&&h){var f=Math.floor(d/l);n=n.slice(0,f)}if(t&&a&&null!=c)for(var g=Jo(c,o,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),y=0;y0,T=null!=t.width&&("truncate"===t.overflow||"break"===t.overflow||"breakAll"===t.overflow),C=i.calculatedLineHeight,D=0;Dl&&ra(n,t.substring(l,u),e,s),ra(n,i[2],e,s,i[1]),l=Ko.lastIndex}lo){b>0?(m.tokens=m.tokens.slice(0,b),y(m,_,x),n.lines=n.lines.slice(0,v+1)):n.lines=n.lines.slice(0,v);break t}var C=w.width,D=null==C||"auto"===C;if("string"==typeof C&&"%"===C.charAt(C.length-1))P.percentWidth=C,h.push(P),P.contentWidth=ur(P.text,I);else{if(D){var A=w.backgroundColor,k=A&&A.image;k&&qo(k=Xo(k))&&(P.width=Math.max(P.width,k.width*T/k.height))}var L=f&&null!=r?r-_:null;null!=L&&L=0&&"right"===(C=x[T]).align;)this._placeToken(C,t,b,f,I,"right",y),w-=C.width,I-=C.width,T--;for(M+=(n-(M-d)-(g-I)-w)/2;S<=T;)C=x[S],this._placeToken(C,t,b,f,M+C.width/2,"center",y),M+=C.width,S++;f+=b}},e.prototype._placeToken=function(t,e,n,i,r,o,s){var l=e.rich[t.styleName]||{};l.text=t.text;var u=t.verticalAlign,h=i+n/2;"top"===u?h=i+t.height/2:"bottom"===u&&(h=i+n-t.height/2),!t.isLineHolder&&Ws(l)&&this._renderBackground(l,e,"right"===o?r-t.width:"center"===o?r-t.width/2:r,h-t.height/2,t.width,t.height);var c=!!l.backgroundColor,p=t.textPadding;p&&(r=Fs(r,o,p),h-=t.height/2-p[0]-t.innerHeight/2);var d=this._getOrCreateChild(vs),f=d.createStyle();d.useStyle(f);var g=this._defaultStyle,y=!1,v=0,m=Bs("fill"in l?l.fill:"fill"in e?e.fill:(y=!0,g.fill)),x=Vs("stroke"in l?l.stroke:"stroke"in e?e.stroke:c||s||g.autoStroke&&!y?null:(v=2,g.stroke)),_=l.textShadowBlur>0||e.textShadowBlur>0;f.text=t.text,f.x=r,f.y=h,_&&(f.shadowBlur=l.textShadowBlur||e.textShadowBlur||0,f.shadowColor=l.textShadowColor||e.textShadowColor||"transparent",f.shadowOffsetX=l.textShadowOffsetX||e.textShadowOffsetX||0,f.shadowOffsetY=l.textShadowOffsetY||e.textShadowOffsetY||0),f.textAlign=o,f.textBaseline="middle",f.font=t.font||a,f.opacity=ot(l.opacity,e.opacity,1),Ns(f,l),x&&(f.lineWidth=ot(l.lineWidth,e.lineWidth,v),f.lineDash=rt(l.lineDash,e.lineDash),f.lineDashOffset=e.lineDashOffset||0,f.stroke=x),m&&(f.fill=m);var b=t.contentWidth,w=t.contentHeight;d.setBoundingRect(new sr(pr(f.x,b,f.textAlign),dr(f.y,w,f.textBaseline),b,w))},e.prototype._renderBackground=function(t,e,n,i,r,o){var a,s,l,u=t.backgroundColor,h=t.borderWidth,c=t.borderColor,p=u&&u.image,d=u&&!p,f=t.borderRadius,g=this;if(d||t.lineHeight||h&&c){(a=this._getOrCreateChild(Cs)).useStyle(a.createStyle()),a.style.fill=null;var y=a.shape;y.x=n,y.y=i,y.width=r,y.height=o,y.r=f,a.dirtyShape()}if(d)(l=a.style).fill=u||null,l.fillOpacity=rt(t.fillOpacity,1);else if(p){(s=this._getOrCreateChild(_s)).onload=function(){g.dirtyStyle()};var v=s.style;v.image=u.image,v.x=n,v.y=i,v.width=r,v.height=o}h&&c&&((l=a.style).lineWidth=h,l.stroke=c,l.strokeOpacity=rt(t.strokeOpacity,1),l.lineDash=t.borderDash,l.lineDashOffset=t.borderDashOffset||0,a.strokeContainThreshold=0,a.hasFill()&&a.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var m=(a||s).style;m.shadowBlur=t.shadowBlur||0,m.shadowColor=t.shadowColor||"transparent",m.shadowOffsetX=t.shadowOffsetX||0,m.shadowOffsetY=t.shadowOffsetY||0,m.opacity=ot(t.opacity,e.opacity,1)},e.makeFont=function(t){var e="";return Es(t)&&(e=[t.fontStyle,t.fontWeight,Rs(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),e&&ut(e)||t.textFont||t.font},e}(da),Ls={left:!0,right:1,center:1},Ps={top:1,bottom:1,middle:1},Os=["fontStyle","fontWeight","fontSize","fontFamily"];function Rs(t){return"string"!=typeof t||-1===t.indexOf("px")&&-1===t.indexOf("rem")&&-1===t.indexOf("em")?isNaN(+t)?"12px":t+"px":t}function Ns(t,e){for(var n=0;n=0,o=!1;if(t instanceof gs){var a=Zs(t),s=r&&a.selectFill||a.normalFill,l=r&&a.selectStroke||a.normalStroke;if(il(s)||il(l)){var u=(i=i||{}).style||{};"inherit"===u.fill?(o=!0,i=A({},i),(u=A({},u)).fill=s):!il(u.fill)&&il(s)?(o=!0,i=A({},i),(u=A({},u)).fill=ol(s)):!il(u.stroke)&&il(l)&&(o||(i=A({},i),u=A({},u)),u.stroke=ol(l)),i.style=u}}if(i&&null==i.z2){o||(i=A({},i));var h=t.z2EmphasisLift;i.z2=t.z2+(null!=h?h:$s)}return i}(this,0,e,n);if("blur"===t)return function(t,e,n){var i=P(t.currentStates,e)>=0,r=t.style.opacity,o=i?null:function(t,e,n,i){for(var r=t.style,o={},a=0;a0){var o={dataIndex:r,seriesIndex:t.seriesIndex};null!=i&&(o.dataType=i),e.push(o)}}))})),e}function Ol(t,e,n){Bl(t,!0),fl(t,vl),Nl(t,e,n)}function Rl(t,e,n,i){i?function(t){Bl(t,!1)}(t):Ol(t,e,n)}function Nl(t,e,n){var i=Hs(t);null!=e?(i.focus=e,i.blurScope=n):i.focus&&(i.focus=null)}var El=["emphasis","blur","select"],zl={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function Vl(t,e,n,i){n=n||"itemStyle";for(var r=0;r1&&(a*=jl(f),s*=jl(f));var g=(r===o?-1:1)*jl((a*a*(s*s)-a*a*(d*d)-s*s*(p*p))/(a*a*(d*d)+s*s*(p*p)))||0,y=g*a*d/s,v=g*-s*p/a,m=(t+n)/2+Kl(c)*y-ql(c)*v,x=(e+i)/2+ql(c)*y+Kl(c)*v,_=tu([1,0],[(p-y)/a,(d-v)/s]),b=[(p-y)/a,(d-v)/s],w=[(-1*p-y)/a,(-1*d-v)/s],S=tu(b,w);if(Ql(b,w)<=-1&&(S=$l),Ql(b,w)>=1&&(S=0),S<0){var M=Math.round(S/$l*1e6)/1e6;S=2*$l+M%2*$l}h.addData(u,m,x,a,s,_,S,c,o)}var nu=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,iu=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var ru=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.applyTransform=function(t){},e}(gs);function ou(t){return null!=t.setData}function au(t,e){var n=function(t){var e=new qa;if(!t)return e;var n,i=0,r=0,o=i,a=r,s=qa.CMD,l=t.match(nu);if(!l)return e;for(var u=0;uk*k+L*L&&(M=T,I=C),{cx:M,cy:I,x0:-h,y0:-c,x1:M*(r/b-1),y1:I*(r/b-1)}}function Iu(t,e){var n,i=bu(e.r,0),r=bu(e.r0||0,0),o=i>0;if(o||r>0){if(o||(i=r,r=0),r>i){var a=i;i=r,r=a}var s=e.startAngle,l=e.endAngle;if(!isNaN(s)&&!isNaN(l)){var u=e.cx,h=e.cy,c=!!e.clockwise,p=xu(l-s),d=p>fu&&p%fu;if(d>Su&&(p=d),i>Su)if(p>fu-Su)t.moveTo(u+i*yu(s),h+i*gu(s)),t.arc(u,h,i,s,l,!c),r>Su&&(t.moveTo(u+r*yu(l),h+r*gu(l)),t.arc(u,h,r,l,s,c));else{var f=void 0,g=void 0,y=void 0,v=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=void 0,I=void 0,T=void 0,C=void 0,D=void 0,A=void 0,k=i*yu(s),L=i*gu(s),P=r*yu(l),O=r*gu(l),R=p>Su;if(R){var N=e.cornerRadius;N&&(f=(n=function(t){var e;if(Y(t)){var n=t.length;if(!n)return t;e=1===n?[t[0],t[0],0,0]:2===n?[t[0],t[0],t[1],t[1]]:3===n?t.concat(t[2]):t}else e=[t,t,t,t];return e}(N))[0],g=n[1],y=n[2],v=n[3]);var E=xu(i-r)/2;if(m=wu(E,y),x=wu(E,v),_=wu(E,f),b=wu(E,g),M=w=bu(m,x),I=S=bu(_,b),(w>Su||S>Su)&&(T=i*yu(l),C=i*gu(l),D=r*yu(s),A=r*gu(s),pSu){var U=wu(y,M),X=wu(v,M),Z=Mu(D,A,k,L,i,U,c),j=Mu(T,C,P,O,i,X,c);t.moveTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),M0&&t.arc(u+Z.cx,h+Z.cy,U,mu(Z.y0,Z.x0),mu(Z.y1,Z.x1),!c),t.arc(u,h,i,mu(Z.cy+Z.y1,Z.cx+Z.x1),mu(j.cy+j.y1,j.cx+j.x1),!c),X>0&&t.arc(u+j.cx,h+j.cy,X,mu(j.y1,j.x1),mu(j.y0,j.x0),!c))}else t.moveTo(u+k,h+L),t.arc(u,h,i,s,l,!c);else t.moveTo(u+k,h+L);if(r>Su&&R)if(I>Su){U=wu(f,I),Z=Mu(P,O,T,C,r,-(X=wu(g,I)),c),j=Mu(k,L,D,A,r,-U,c);t.lineTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),I0&&t.arc(u+Z.cx,h+Z.cy,X,mu(Z.y0,Z.x0),mu(Z.y1,Z.x1),!c),t.arc(u,h,r,mu(Z.cy+Z.y1,Z.cx+Z.x1),mu(j.cy+j.y1,j.cx+j.x1),c),U>0&&t.arc(u+j.cx,h+j.cy,U,mu(j.y1,j.x1),mu(j.y0,j.x0),!c))}else t.lineTo(u+P,h+O),t.arc(u,h,r,l,s,c);else t.lineTo(u+P,h+O)}else t.moveTo(u,h);t.closePath()}}}var Tu=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},Cu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Tu},e.prototype.buildPath=function(t,e){Iu(t,e)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(gs);Cu.prototype.type="sector";var Du=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Au=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Du},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)},e}(gs);function ku(t,e,n){var i=e.smooth,r=e.points;if(r&&r.length>=2){if(i){var o=function(t,e,n,i){var r,o,a,s,l=[],u=[],h=[],c=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var p=0,d=t.length;pqu[1]){if(a=!1,r)return a;var u=Math.abs(qu[0]-ju[1]),h=Math.abs(ju[0]-qu[1]);Math.min(u,h)>i.len()&&(u0){var c={duration:h.duration,delay:h.delay||0,easing:h.easing,done:o,force:!!o||!!a,setToFinal:!u,scope:t,during:a};l?e.animateFrom(n,c):e.animateTo(n,c)}else e.stopAnimation(),!l&&e.attr(n),a&&a(1),o&&o()}function rh(t,e,n,i,r,o){ih("update",t,e,n,i,r,o)}function oh(t,e,n,i,r,o){ih("enter",t,e,n,i,r,o)}function ah(t){if(!t.__zr)return!0;for(var e=0;eMath.abs(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function Dh(t){return!t.isGroup}function Ah(t,e,n){if(t&&e){var i,r=(i={},t.traverse((function(t){Dh(t)&&t.anid&&(i[t.anid]=t)})),i);e.traverse((function(t){if(Dh(t)&&t.anid){var e=r[t.anid];if(e){var i=o(t);t.attr(o(e)),rh(t,i,n,Hs(t).dataIndex)}}}))}function o(t){var e={x:t.x,y:t.y,rotation:t.rotation};return function(t){return null!=t.shape}(t)&&(e.shape=A({},t.shape)),e}}function kh(t,e){return z(t,(function(t){var n=t[0];n=ch(n,e.x),n=ph(n,e.x+e.width);var i=t[1];return i=ch(i,e.y),[n,i=ph(i,e.y+e.height)]}))}function Lh(t,e){var n=ch(t.x,e.x),i=ph(t.x+t.width,e.x+e.width),r=ch(t.y,e.y),o=ph(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}}function Ph(t,e,n){var i=A({rectHover:!0},e),r=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(r.image=t.slice(8),k(r,n),new _s(i)):xh(t.replace("path://",""),i,n,"center")}function Oh(t,e,n,i,r){for(var o=0,a=r[r.length-1];o=-1e-6)return!1;var f=t-r,g=e-o,y=Nh(f,g,u,h)/d;if(y<0||y>1)return!1;var v=Nh(f,g,c,p)/d;return!(v<0||v>1)}function Nh(t,e,n,i){return t*i-n*e}function Eh(t){var e=t.itemTooltipOption,n=t.componentModel,i=t.itemName,r=X(e)?{formatter:e}:e,o=n.mainType,a=n.componentIndex,s={componentType:o,name:i,$vars:["name"]};s[o+"Index"]=a;var l=t.formatterParamsExtra;l&&E(G(l),(function(t){mt(s,t)||(s[t]=l[t],s.$vars.push(t))}));var u=Hs(t.el);u.componentMainType=o,u.componentIndex=a,u.tooltipConfig={name:i,option:k({content:i,formatterParams:s},r)}}function zh(t,e){var n;t.isGroup&&(n=e(t)),n||t.traverse(e)}function Vh(t,e){if(t)if(Y(t))for(var n=0;n-1?vc:xc;function Sc(t,e){t=t.toUpperCase(),bc[t]=new dc(e),_c[t]=e}function Mc(t){return bc[t]}Sc(mc,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),Sc(vc,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Ic=1e3,Tc=6e4,Cc=36e5,Dc=864e5,Ac=31536e6,kc={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Lc="{yyyy}-{MM}-{dd}",Pc={year:"{yyyy}",month:"{yyyy}-{MM}",day:Lc,hour:"{yyyy}-{MM}-{dd} "+kc.hour,minute:"{yyyy}-{MM}-{dd} "+kc.minute,second:"{yyyy}-{MM}-{dd} "+kc.second,millisecond:kc.none},Oc=["year","month","day","hour","minute","second","millisecond"],Rc=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Nc(t,e){return"0000".substr(0,e-(t+="").length)+t}function Ec(t){switch(t){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return t}}function zc(t){return t===Ec(t)}function Vc(t,e,n,i){var r=jr(t),o=r[Gc(n)](),a=r[Wc(n)]()+1,s=Math.floor((a-1)/3)+1,l=r[Hc(n)](),u=r["get"+(n?"UTC":"")+"Day"](),h=r[Yc(n)](),c=(h-1)%12+1,p=r[Uc(n)](),d=r[Xc(n)](),f=r[Zc(n)](),g=(i instanceof dc?i:Mc(i||wc)||bc.EN).getModel("time"),y=g.get("month"),v=g.get("monthAbbr"),m=g.get("dayOfWeek"),x=g.get("dayOfWeekAbbr");return(e||"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,o%100+"").replace(/{Q}/g,s+"").replace(/{MMMM}/g,y[a-1]).replace(/{MMM}/g,v[a-1]).replace(/{MM}/g,Nc(a,2)).replace(/{M}/g,a+"").replace(/{dd}/g,Nc(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,m[u]).replace(/{ee}/g,x[u]).replace(/{e}/g,u+"").replace(/{HH}/g,Nc(h,2)).replace(/{H}/g,h+"").replace(/{hh}/g,Nc(c+"",2)).replace(/{h}/g,c+"").replace(/{mm}/g,Nc(p,2)).replace(/{m}/g,p+"").replace(/{ss}/g,Nc(d,2)).replace(/{s}/g,d+"").replace(/{SSS}/g,Nc(f,3)).replace(/{S}/g,f+"")}function Bc(t,e){var n=jr(t),i=n[Wc(e)]()+1,r=n[Hc(e)](),o=n[Yc(e)](),a=n[Uc(e)](),s=n[Xc(e)](),l=0===n[Zc(e)](),u=l&&0===s,h=u&&0===a,c=h&&0===o,p=c&&1===r;return p&&1===i?"year":p?"month":c?"day":h?"hour":u?"minute":l?"second":"millisecond"}function Fc(t,e,n){var i=j(t)?jr(t):t;switch(e=e||Bc(t,n)){case"year":return i[Gc(n)]();case"half-year":return i[Wc(n)]()>=6?1:0;case"quarter":return Math.floor((i[Wc(n)]()+1)/4);case"month":return i[Wc(n)]();case"day":return i[Hc(n)]();case"half-day":return i[Yc(n)]()/24;case"hour":return i[Yc(n)]();case"minute":return i[Uc(n)]();case"second":return i[Xc(n)]();case"millisecond":return i[Zc(n)]()}}function Gc(t){return t?"getUTCFullYear":"getFullYear"}function Wc(t){return t?"getUTCMonth":"getMonth"}function Hc(t){return t?"getUTCDate":"getDate"}function Yc(t){return t?"getUTCHours":"getHours"}function Uc(t){return t?"getUTCMinutes":"getMinutes"}function Xc(t){return t?"getUTCSeconds":"getSeconds"}function Zc(t){return t?"getUTCMilliseconds":"getMilliseconds"}function jc(t){return t?"setUTCFullYear":"setFullYear"}function qc(t){return t?"setUTCMonth":"setMonth"}function Kc(t){return t?"setUTCDate":"setDate"}function $c(t){return t?"setUTCHours":"setHours"}function Jc(t){return t?"setUTCMinutes":"setMinutes"}function Qc(t){return t?"setUTCSeconds":"setSeconds"}function tp(t){return t?"setUTCMilliseconds":"setMilliseconds"}function ep(t){if(!eo(t))return X(t)?t:"-";var e=(t+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function np(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,(function(t,e){return e.toUpperCase()})),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}var ip=st,rp=/([&<>"'])/g,op={"&":"&","<":"<",">":">",'"':""","'":"'"};function ap(t){return null==t?"":(t+"").replace(rp,(function(t,e){return op[e]}))}function sp(t,e,n){function i(t){return t&&ut(t)?t:"-"}function r(t){return!(null==t||isNaN(t)||!isFinite(t))}var o="time"===e,a=t instanceof Date;if(o||a){var s=o?jr(t):t;if(!isNaN(+s))return Vc(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(a)return"-"}if("ordinal"===e)return Z(t)?i(t):j(t)&&r(t)?t+"":"-";var l=to(t);return r(l)?ep(l):Z(t)?i(t):"boolean"==typeof t?t+"":"-"}var lp=["a","b","c","d","e","f","g"],up=function(t,e){return"{"+t+(null==e?"":e)+"}"};function hp(t,e,n){Y(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o':'':{renderMode:o,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===r?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function pp(t,e){return e=e||"transparent",X(t)?t:q(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}function dp(t,e){if("_blank"===e||"blank"===e){var n=window.open();n.opener=null,n.location.href=t}else window.open(t,e)}var fp=E,gp=["left","right","top","bottom","width","height"],yp=[["width","left","right"],["height","top","bottom"]];function vp(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild((function(l,u){var h,c,p=l.getBoundingRect(),d=e.childAt(u+1),f=d&&d.getBoundingRect();if("horizontal"===t){var g=p.width+(f?-f.x+p.x:0);(h=o+g)>i||l.newline?(o=0,h=g,a+=s+n,s=p.height):s=Math.max(s,p.height)}else{var y=p.height+(f?-f.y+p.y:0);(c=a+y)>r||l.newline?(o+=s+n,a=0,c=y,s=p.width):s=Math.max(s,p.width)}l.newline||(l.x=o,l.y=a,l.markRedraw(),"horizontal"===t?o=h+n:a=c+n)}))}var mp=vp;H(vp,"vertical"),H(vp,"horizontal");function xp(t,e,n){n=ip(n||0);var i=e.width,r=e.height,o=Er(t.left,i),a=Er(t.top,r),s=Er(t.right,i),l=Er(t.bottom,r),u=Er(t.width,i),h=Er(t.height,r),c=n[2]+n[0],p=n[1]+n[3],d=t.aspect;switch(isNaN(u)&&(u=i-s-p-o),isNaN(h)&&(h=r-l-c-a),null!=d&&(isNaN(u)&&isNaN(h)&&(d>i/r?u=.8*i:h=.8*r),isNaN(u)&&(u=d*h),isNaN(h)&&(h=u/d)),isNaN(o)&&(o=i-s-u-p),isNaN(a)&&(a=r-l-h-c),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-p}switch(t.top||t.bottom){case"middle":case"center":a=r/2-h/2-n[0];break;case"bottom":a=r-h-c}o=o||0,a=a||0,isNaN(u)&&(u=i-p-o-(s||0)),isNaN(h)&&(h=r-c-a-(l||0));var f=new sr(o+n[3],a+n[0],u,h);return f.margin=n,f}function _p(t,e,n,i,r,o){var a,s=!r||!r.hv||r.hv[0],l=!r||!r.hv||r.hv[1],u=r&&r.boundingMode||"all";if((o=o||t).x=t.x,o.y=t.y,!s&&!l)return!1;if("raw"===u)a="group"===t.type?new sr(0,0,+e.width||0,+e.height||0):t.getBoundingRect();else if(a=t.getBoundingRect(),t.needLocalTransform()){var h=t.getLocalTransform();(a=a.clone()).applyTransform(h)}var c=xp(k({width:a.width,height:a.height},e),n,i),p=s?c.x-a.x:0,d=l?c.y-a.y:0;return"raw"===u?(o.x=p,o.y=d):(o.x+=p,o.y+=d),o===t&&t.markRedraw(),!0}function bp(t){var e=t.layoutMode||t.constructor.layoutMode;return q(e)?e:e?{type:e}:null}function wp(t,e,n){var i=n&&n.ignoreSize;!Y(i)&&(i=[i,i]);var r=a(yp[0],0),o=a(yp[1],1);function a(n,r){var o={},a=0,u={},h=0;if(fp(n,(function(e){u[e]=t[e]})),fp(n,(function(t){s(e,t)&&(o[t]=u[t]=e[t]),l(o,t)&&a++,l(u,t)&&h++})),i[r])return l(e,n[1])?u[n[2]]=null:l(e,n[2])&&(u[n[1]]=null),u;if(2!==h&&a){if(a>=2)return o;for(var c=0;c=0;a--)o=C(o,n[a],!0);e.defaultOption=o}return e.defaultOption},e.prototype.getReferringComponents=function(t,e){var n=t+"Index",i=t+"Id";return Ao(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},e)},e.prototype.getBoxLayoutParams=function(){var t=this;return{left:t.get("left"),top:t.get("top"),right:t.get("right"),bottom:t.get("bottom"),width:t.get("width"),height:t.get("height")}},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=function(){var t=e.prototype;t.type="component",t.id="",t.name="",t.mainType="",t.subType="",t.componentIndex=0}(),e}(dc);zo(Tp,dc),Go(Tp),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){var i=No(t);e[i.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=No(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(Tp),function(t,e){function n(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}t.topologicalTravel=function(t,i,r,o){if(t.length){var a=function(t){var i={},r=[];return E(t,(function(o){var a=n(i,o),s=function(t,e){var n=[];return E(t,(function(t){P(e,t)>=0&&n.push(t)})),n}(a.originalDeps=e(o),t);a.entryCount=s.length,0===a.entryCount&&r.push(o),E(s,(function(t){P(a.predecessor,t)<0&&a.predecessor.push(t);var e=n(i,t);P(e.successor,t)<0&&e.successor.push(o)}))})),{graph:i,noEntryList:r}}(i),s=a.graph,l=a.noEntryList,u={};for(E(t,(function(t){u[t]=!0}));l.length;){var h=l.pop(),c=s[h],p=!!u[h];p&&(r.call(o,h,c.originalDeps.slice()),delete u[h]),E(c.successor,p?f:d)}E(u,(function(){var t="";throw new Error(t)}))}function d(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}function f(t){u[t]=!0,d(t)}}}(Tp,(function(t){var e=[];E(Tp.getClassesByMainType(t),(function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])})),e=z(e,(function(t){return No(t).main})),"dataset"!==t&&P(e,"dataset")<=0&&e.unshift("dataset");return e}));var Cp="";"undefined"!=typeof navigator&&(Cp=navigator.platform||"");var Dp="rgba(0, 0, 0, 0.2)",Ap={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Dp,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Dp,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Dp,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Dp,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Dp,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Dp,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Cp.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},kp=ft(["tooltip","label","itemName","itemId","itemGroupId","seriesName"]),Lp="original",Pp="arrayRows",Op="objectRows",Rp="keyedColumns",Np="typedArray",Ep="unknown",zp="column",Vp="row",Bp=1,Fp=2,Gp=3,Wp=So();function Hp(t,e,n){var i={},r=Up(e);if(!r||!t)return i;var o,a,s=[],l=[],u=e.ecModel,h=Wp(u).datasetMap,c=r.uid+"_"+n.seriesLayoutBy;E(t=t.slice(),(function(e,n){var r=q(e)?e:t[n]={name:e};"ordinal"===r.type&&null==o&&(o=n,a=f(r)),i[r.name]=[]}));var p=h.get(c)||h.set(c,{categoryWayDim:a,valueWayDim:0});function d(t,e,n){for(var i=0;ie)return t[i];return t[n-1]}(i,a):n;if((h=h||n)&&h.length){var c=h[l];return r&&(u[r]=c),s.paletteIdx=(l+1)%h.length,c}}var id=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(t,e,n,i,r,o){i=i||{},this.option=null,this._theme=new dc(i),this._locale=new dc(r),this._optionManager=o},e.prototype.setOption=function(t,e,n){var i=ad(e);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,e){return this._resetOption(t,ad(e))},e.prototype._resetOption=function(t,e){var n=!1,i=this._optionManager;if(!t||"recreate"===t){var r=i.mountOption("recreate"===t);0,this.option&&"recreate"!==t?(this.restoreData(),this._mergeOption(r,e)):$p(this,r),n=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var o=i.getTimelineOption(this);o&&(n=!0,this._mergeOption(o,e))}if(!t||"recreate"===t||"media"===t){var a=i.getMediaOption(this);a.length&&E(a,(function(t){n=!0,this._mergeOption(t,e)}),this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,e){var n=this.option,i=this._componentsMap,r=this._componentsCount,o=[],a=ft(),s=e&&e.replaceMergeMainTypeMap;Wp(this).datasetMap=ft(),E(t,(function(t,e){null!=t&&(Tp.hasClass(e)?e&&(o.push(e),a.set(e,!0)):n[e]=null==n[e]?T(t):C(n[e],t,!0))})),s&&s.each((function(t,e){Tp.hasClass(e)&&!a.get(e)&&(o.push(e),a.set(e,!0))})),Tp.topologicalTravel(o,Tp.getAllClassMainTypes(),(function(e){var o=function(t,e,n){var i=jp.get(e);if(!i)return n;var r=i(t);return r?n.concat(r):n}(this,e,ho(t[e])),a=i.get(e),l=a?s&&s.get(e)?"replaceMerge":"normalMerge":"replaceAll",u=yo(a,o,l);(function(t,e,n){E(t,(function(t){var i=t.newOption;q(i)&&(t.keyInfo.mainType=e,t.keyInfo.subType=function(t,e,n,i){return e.type?e.type:n?n.subType:i.determineSubType(t,e)}(e,i,t.existing,n))}))})(u,e,Tp),n[e]=null,i.set(e,null),r.set(e,0);var h,c=[],p=[],d=0;E(u,(function(t,n){var i=t.existing,r=t.newOption;if(r){var o="series"===e,a=Tp.getClass(e,t.keyInfo.subType,!o);if(!a)return;if("tooltip"===e){if(h)return void 0;h=!0}if(i&&i.constructor===a)i.name=t.keyInfo.name,i.mergeOption(r,this),i.optionUpdated(r,!1);else{var s=A({componentIndex:n},t.keyInfo);A(i=new a(r,this,this,s),s),t.brandNew&&(i.__requireNewView=!0),i.init(r,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(c.push(i.option),p.push(i),d++):(c.push(void 0),p.push(void 0))}),this),n[e]=c,i.set(e,p),r.set(e,d),"series"===e&&qp(this)}),this),this._seriesIndices||qp(this)},e.prototype.getOption=function(){var t=T(this.option);return E(t,(function(e,n){if(Tp.hasClass(n)){for(var i=ho(e),r=i.length,o=!1,a=r-1;a>=0;a--)i[a]&&!bo(i[a])?o=!0:(i[a]=null,!o&&r--);i.length=r,t[n]=i}})),delete t["\0_ec_inner"],t},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,e){var n=this._componentsMap.get(t);if(n){var i=n[e||0];if(i)return i;if(null==e)for(var r=0;r=e:"max"===n?t<=e:t===e})(i[a],t,o)||(r=!1)}})),r}var fd=E,gd=q,yd=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function vd(t){var e=t&&t.itemStyle;if(e)for(var n=0,i=yd.length;n=0;g--){var y=t[g];if(s||(p=y.data.rawIndexOf(y.stackedByDimension,c)),p>=0){var v=y.data.getByRawIndex(y.stackResultDimension,p);if("all"===l||"positive"===l&&v>0||"negative"===l&&v<0||"samesign"===l&&d>=0&&v>0||"samesign"===l&&d<=0&&v<0){d=Hr(d,v),f=v;break}}}return i[0]=d,i[1]=f,i}))}))}var Nd,Ed,zd,Vd,Bd,Fd=function(t){this.data=t.data||(t.sourceFormat===Rp?{}:[]),this.sourceFormat=t.sourceFormat||Ep,this.seriesLayoutBy=t.seriesLayoutBy||zp,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var e=this.dimensionsDefine=t.dimensionsDefine;if(e)for(var n=0;nu&&(u=d)}s[0]=l,s[1]=u}},i=function(){return this._data?this._data.length/this._dimSize:0};function r(t){for(var e=0;e=0&&(s=o.interpolatedValue[l])}return null!=s?s+"":""})):void 0},t.prototype.getRawValue=function(t,e){return af(this.getData(e),t)},t.prototype.formatTooltip=function(t,e,n){},t}();function uf(t){var e,n;return q(t)?t.type&&(n=t):e=t,{text:e,frag:n}}function hf(t){return new cf(t)}var cf=function(){function t(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return t.prototype.perform=function(t){var e,n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(e=this._plan(this.context));var o,a=h(this._modBy),s=this._modDataCount||0,l=h(t&&t.modBy),u=t&&t.modDataCount||0;function h(t){return!(t>=1)&&(t=1),t}a===l&&s===u||(e="reset"),(this._dirty||"reset"===e)&&(this._dirty=!1,o=this._doReset(i)),this._modBy=l,this._modDataCount=u;var c=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var p=this._dueIndex,d=Math.min(null!=c?this._dueIndex+c:1/0,this._dueEnd);if(!i&&(o||p1&&i>0?s:a}};return o;function a(){return e=t?null:oe},gte:function(t,e){return t>=e}},vf=function(){function t(t,e){if(!j(e)){var n="";0,ao(n)}this._opFn=yf[t],this._rvalFloat=to(e)}return t.prototype.evaluate=function(t){return j(t)?this._opFn(t,this._rvalFloat):this._opFn(to(t),this._rvalFloat)},t}(),mf=function(){function t(t,e){var n="desc"===t;this._resultLT=n?1:-1,null==e&&(e=n?"min":"max"),this._incomparable="min"===e?-1/0:1/0}return t.prototype.evaluate=function(t,e){var n=j(t)?t:to(t),i=j(e)?e:to(e),r=isNaN(n),o=isNaN(i);if(r&&(n=this._incomparable),o&&(i=this._incomparable),r&&o){var a=X(t),s=X(e);a&&(n=s?t:0),s&&(i=a?e:0)}return ni?-this._resultLT:0},t}(),xf=function(){function t(t,e){this._rval=e,this._isEQ=t,this._rvalTypeof=typeof e,this._rvalFloat=to(e)}return t.prototype.evaluate=function(t){var e=t===this._rval;if(!e){var n=typeof t;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(e=to(t)===this._rvalFloat)}return this._isEQ?e:!e},t}();function _f(t,e){return"eq"===t||"ne"===t?new xf("eq"===t,e):mt(yf,t)?new vf(t,e):null}var bf=function(){function t(){}return t.prototype.getRawData=function(){throw new Error("not supported")},t.prototype.getRawDataItem=function(t){throw new Error("not supported")},t.prototype.cloneRawData=function(){},t.prototype.getDimensionInfo=function(t){},t.prototype.cloneAllDimensionInfo=function(){},t.prototype.count=function(){},t.prototype.retrieveValue=function(t,e){},t.prototype.retrieveValueFromItem=function(t,e){},t.prototype.convertValue=function(t,e){return df(t,e)},t}();function wf(t){var e=t.sourceFormat;if(!Df(e)){var n="";0,ao(n)}return t.data}function Sf(t){var e=t.sourceFormat,n=t.data;if(!Df(e)){var i="";0,ao(i)}if(e===Pp){for(var r=[],o=0,a=n.length;o65535?Lf:Pf}function zf(t,e,n,i,r){var o=Nf[n||"float"];if(r){var a=t[e],s=a&&a.length;if(s!==i){for(var l=new o(i),u=0;ug[1]&&(g[1]=f)}return this._rawCount=this._count=s,{start:a,end:s}},t.prototype._initDataFromProvider=function(t,e,n){for(var i=this._provider,r=this._chunks,o=this._dimensions,a=o.length,s=this._rawExtent,l=z(o,(function(t){return t.property})),u=0;uy[1]&&(y[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=e,this._extent=[]},t.prototype.count=function(){return this._count},t.prototype.get=function(t,e){if(!(e>=0&&e=0&&e=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&nt))return o;r=o-1}}return-1},t.prototype.indicesOfNearest=function(t,e,n){var i=this._chunks[t],r=[];if(!i)return r;null==n&&(n=1/0);for(var o=1/0,a=-1,s=0,l=0,u=this.count();l=0&&a<0)&&(o=c,a=h,s=0),h===a&&(r[s++]=l))}return r.length=s,r},t.prototype.getIndices=function(){var t,e=this._indices;if(e){var n=e.constructor,i=this._count;if(n===Array){t=new n(i);for(var r=0;r=u&&x<=h||isNaN(x))&&(a[s++]=d),d++}p=!0}else if(2===r){f=c[i[0]];var y=c[i[1]],v=t[i[1]][0],m=t[i[1]][1];for(g=0;g=u&&x<=h||isNaN(x))&&(_>=v&&_<=m||isNaN(_))&&(a[s++]=d),d++}p=!0}}if(!p)if(1===r)for(g=0;g=u&&x<=h||isNaN(x))&&(a[s++]=b)}else for(g=0;gt[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return sy[1]&&(y[1]=g)}}}},t.prototype.lttbDownSample=function(t,e){var n,i,r,o=this.clone([t],!0),a=o._chunks[t],s=this.count(),l=0,u=Math.floor(1/e),h=this.getRawIndex(0),c=new(Ef(this._rawCount))(Math.min(2*(Math.ceil(s/u)+2),s));c[l++]=h;for(var p=1;pn&&(n=i,r=I)}M>0&&M<_-x&&(c[l++]=Math.min(S,r),r=Math.max(S,r)),c[l++]=r,h=r}return c[l++]=this.getRawIndex(s-1),o._count=l,o._indices=c,o.getRawIndex=this._getRawIdx,o},t.prototype.downSample=function(t,e,n,i){for(var r=this.clone([t],!0),o=r._chunks,a=[],s=Math.floor(1/e),l=o[t],u=this.count(),h=r._rawExtent[t]=[1/0,-1/0],c=new(Ef(this._rawCount))(Math.ceil(u/s)),p=0,d=0;du-d&&(s=u-d,a.length=s);for(var f=0;fh[1]&&(h[1]=y),c[p++]=v}return r._count=p,r._indices=c,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();ra&&(a=l)}return i=[o,a],this._extent[t]=i,i},t.prototype.getRawDataItem=function(t){var e=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(e);for(var n=[],i=this._chunks,r=0;r=0?this._indices[t]:-1},t.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},t.internalField=function(){function t(t,e,n,i){return df(t[i],this._dimensions[i])}Af={arrayRows:t,objectRows:function(t,e,n,i){return df(t[e],this._dimensions[i])},keyedColumns:t,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return df(r instanceof Array?r[i]:r,this._dimensions[i])},typedArray:function(t,e,n,i){return t[i]}}}(),t}(),Bf=function(){function t(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return t.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},t.prototype._setLocalSource=function(t,e){this._sourceList=t,this._upstreamSignList=e,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},t.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},t.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},t.prototype._createSource=function(){this._setLocalSource([],[]);var t,e,n=this._sourceHost,i=this._getUpstreamSourceManagers(),r=!!i.length;if(Gf(n)){var o=n,a=void 0,s=void 0,l=void 0;if(r){var u=i[0];u.prepareSource(),a=(l=u.getSource()).data,s=l.sourceFormat,e=[u._getVersionSign()]}else s=$(a=o.get("data",!0))?Np:Lp,e=[];var h=this._getSourceMetaRawOption()||{},c=l&&l.metaRawOption||{},p=rt(h.seriesLayoutBy,c.seriesLayoutBy)||null,d=rt(h.sourceHeader,c.sourceHeader),f=rt(h.dimensions,c.dimensions);t=p!==c.seriesLayoutBy||!!d!=!!c.sourceHeader||f?[Wd(a,{seriesLayoutBy:p,sourceHeader:d,dimensions:f},s)]:[]}else{var g=n;if(r){var y=this._applyTransform(i);t=y.sourceList,e=y.upstreamSignList}else{t=[Wd(g.get("source",!0),this._getSourceMetaRawOption(),null)],e=[]}}this._setLocalSource(t,e)},t.prototype._applyTransform=function(t){var e,n=this._sourceHost,i=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(null!=r){var o="";1!==t.length&&Wf(o)}var a,s=[],l=[];return E(t,(function(t){t.prepareSource();var e=t.getSource(r||0),n="";null==r||e||Wf(n),s.push(e),l.push(t._getVersionSign())})),i?e=function(t,e,n){var i=ho(t),r=i.length,o="";r||ao(o);for(var a=0,s=r;a1||n>0&&!t.noHeader;return E(t.blocks,(function(t){var n=qf(t);n>=e&&(e=n+ +(i&&(!n||Zf(t)&&!t.noHeader)))})),e}return 0}function Kf(t,e,n,i){var r,o=e.noHeader,a=(r=qf(e),{html:Yf[r],richText:Uf[r]}),s=[],l=e.blocks||[];lt(!l||Y(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var h={valueAsc:"asc",valueDesc:"desc"};if(mt(h,u)){var c=new mf(h[u],null);l.sort((function(t,e){return c.evaluate(t.sortParam,e.sortParam)}))}else"seriesDesc"===u&&l.reverse()}E(l,(function(n,r){var o=e.valueFormatter,l=jf(n)(o?A(A({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)}));var p="richText"===t.renderMode?s.join(a.richText):Qf(s.join(""),o?n:a.html);if(o)return p;var d=sp(e.header,"ordinal",t.useUTC),f=Hf(i,t.renderMode).nameStyle;return"richText"===t.renderMode?tg(t,d,f)+a.richText+p:Qf('
'+ap(d)+"
"+p,n)}function $f(t,e,n,i){var r=t.renderMode,o=e.noName,a=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,h=e.valueFormatter||t.valueFormatter||function(t){return z(t=Y(t)?t:[t],(function(t,e){return sp(t,Y(d)?d[e]:d,u)}))};if(!o||!a){var c=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||"#333",r),p=o?"":sp(l,"ordinal",u),d=e.valueType,f=a?[]:h(e.value),g=!s||!o,y=!s&&o,v=Hf(i,r),m=v.nameStyle,x=v.valueStyle;return"richText"===r?(s?"":c)+(o?"":tg(t,p,m))+(a?"":function(t,e,n,i,r){var o=[r],a=i?10:20;return n&&o.push({padding:[0,0,0,a],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(Y(e)?e.join(" "):e,o)}(t,f,g,y,x)):Qf((s?"":c)+(o?"":function(t,e,n){return''+ap(t)+""}(p,!s,m))+(a?"":function(t,e,n,i){var r=n?"10px":"20px",o=e?"float:right;margin-left:"+r:"";return t=Y(t)?t:[t],''+z(t,(function(t){return ap(t)})).join("  ")+""}(f,g,y,x)),n)}}function Jf(t,e,n,i,r,o){if(t)return jf(t)({useUTC:r,renderMode:n,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter},t,0,o)}function Qf(t,e){return'
'+t+'
'}function tg(t,e,n){return t.markupStyleCreator.wrapRichTextStyle(e,n)}function eg(t,e){return pp(t.getData().getItemVisual(e,"style")[t.visualDrawType])}function ng(t,e){var n=t.get("padding");return null!=n?n:"richText"===e?[8,10]:10}var ig=function(){function t(){this.richTextStyles={},this._nextStyleNameId=no()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(t,e,n){var i="richText"===n?this._generateStyleName():null,r=cp({color:e,type:t,renderMode:n,markerId:i});return X(r)?r:(this.richTextStyles[i]=r.style,r.content)},t.prototype.wrapRichTextStyle=function(t,e){var n={};Y(e)?E(e,(function(t){return A(n,t)})):A(n,e);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+t+"}"},t}();function rg(t){var e,n,i,r,o=t.series,a=t.dataIndex,s=t.multipleSeries,l=o.getData(),u=l.mapDimensionsAll("defaultedTooltip"),h=u.length,c=o.getRawValue(a),p=Y(c),d=eg(o,a);if(h>1||p&&!h){var f=function(t,e,n,i,r){var o=e.getData(),a=V(t,(function(t,e,n){var i=o.getDimensionInfo(n);return t||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],u=[];function h(t,e){var n=o.getDimensionInfo(e);n&&!1!==n.otherDims.tooltip&&(a?u.push(Xf("nameValue",{markerType:"subItem",markerColor:r,name:n.displayName,value:t,valueType:n.type})):(s.push(t),l.push(n.type)))}return i.length?E(i,(function(t){h(af(o,n,t),t)})):E(t,h),{inlineValues:s,inlineValueTypes:l,blocks:u}}(c,o,a,u,d);e=f.inlineValues,n=f.inlineValueTypes,i=f.blocks,r=f.inlineValues[0]}else if(h){var g=l.getDimensionInfo(u[0]);r=e=af(l,a,u[0]),n=g.type}else r=e=p?c[0]:c;var y=_o(o),v=y&&o.name||"",m=l.getName(a),x=s?v:m;return Xf("section",{header:v,noHeader:s||!y,sortParam:r,blocks:[Xf("nameValue",{markerType:"item",markerColor:d,name:x,noName:!ut(x),value:e,valueType:n})].concat(i||[])})}var og=So();function ag(t,e){return t.getName(e)||t.getId(e)}var sg=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._selectedDataIndicesMap={},e}return n(e,t),e.prototype.init=function(t,e,n){this.seriesIndex=this.componentIndex,this.dataTask=hf({count:ug,reset:hg}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),(og(this).sourceManager=new Bf(this)).prepareSource();var i=this.getInitialData(t,n);pg(i,this),this.dataTask.context.data=i,og(this).dataBeforeProcessed=i,lg(this),this._initSelectedMapFromData(i)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=bp(this),i=n?Sp(t):{},r=this.subType;Tp.hasClass(r)&&(r+="Series"),C(t,e.getTheme().get(this.subType)),C(t,this.getDefaultOption()),co(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&wp(t,i,n)},e.prototype.mergeOption=function(t,e){t=C(this.option,t,!0),this.fillDataTextStyle(t.data);var n=bp(this);n&&wp(this.option,t,n);var i=og(this).sourceManager;i.dirty(),i.prepareSource();var r=this.getInitialData(t,e);pg(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,og(this).dataBeforeProcessed=r,lg(this),this._initSelectedMapFromData(r)},e.prototype.fillDataTextStyle=function(t){if(t&&!$(t))for(var e=["show"],n=0;nthis.getShallow("animationThreshold")&&(e=!1),!!e},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,e,n){var i=this.ecModel,r=td.prototype.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,e){this._innerSelect(this.getData(e),t)},e.prototype.unselect=function(t,e){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,r=this.getData(e);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var o=0;o=0&&n.push(r)}return n},e.prototype.isSelected=function(t,e){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(e);return("all"===n||n[ag(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this.__universalTransitionEnabled)return!0;var t=this.option.universalTransition;return!!t&&(!0===t||t&&t.enabled)},e.prototype._innerSelect=function(t,e){var n,i,r=this.option,o=r.selectedMode,a=e.length;if(o&&a)if("series"===o)r.selectedMap="all";else if("multiple"===o){q(r.selectedMap)||(r.selectedMap={});for(var s=r.selectedMap,l=0;l0&&this._innerSelect(t,e)}},e.registerClass=function(t){return Tp.registerClass(t)},e.protoInitialize=function(){var t=e.prototype;t.type="series.__base__",t.seriesIndex=0,t.ignoreStyleOnData=!1,t.hasSymbolVisual=!1,t.defaultSymbol="circle",t.visualStyleAccessPath="itemStyle",t.visualDrawType="fill"}(),e}(Tp);function lg(t){var e=t.name;_o(t)||(t.name=function(t){var e=t.getRawData(),n=e.mapDimensionsAll("seriesName"),i=[];return E(n,(function(t){var n=e.getDimensionInfo(t);n.displayName&&i.push(n.displayName)})),i.join(" ")}(t)||e)}function ug(t){return t.model.getRawData().count()}function hg(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),cg}function cg(t,e){e.outputData&&t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function pg(t,e){E(gt(t.CHANGABLE_METHODS,t.DOWNSAMPLE_METHODS),(function(n){t.wrapMethod(n,H(dg,e))}))}function dg(t,e){var n=fg(t);return n&&n.setOutputEnd((e||this).count()),e}function fg(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}R(sg,lf),R(sg,td),zo(sg,Tp);var gg=function(){function t(){this.group=new Cr,this.uid=gc("viewComponent")}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){},t.prototype.updateLayout=function(t,e,n,i){},t.prototype.updateVisual=function(t,e,n,i){},t.prototype.toggleBlurSeries=function(t,e,n){},t.prototype.eachRendered=function(t){var e=this.group;e&&e.traverse(t)},t}();function yg(){var t=So();return function(e){var n=t(e),i=e.pipelineContext,r=!!n.large,o=!!n.progressiveRender,a=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(r===a&&o===s)&&"reset"}}Eo(gg),Go(gg);var vg=So(),mg=yg(),xg=function(){function t(){this.group=new Cr,this.uid=gc("viewChart"),this.renderTask=hf({plan:wg,reset:Sg}),this.renderTask.context={view:this}}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){0},t.prototype.highlight=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&bg(r,i,"emphasis")},t.prototype.downplay=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&bg(r,i,"normal")},t.prototype.remove=function(t,e){this.group.removeAll()},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateLayout=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.eachRendered=function(t){Vh(this.group,t)},t.markUpdateMethod=function(t,e){vg(t).updateMethod=e},t.protoInitialize=void(t.prototype.type="chart"),t}();function _g(t,e,n){t&&Fl(t)&&("emphasis"===e?_l:bl)(t,n)}function bg(t,e,n){var i=wo(t,e),r=e&&null!=e.highlightKey?function(t){var e=Xs[t];return null==e&&Us<=32&&(e=Xs[t]=Us++),e}(e.highlightKey):null;null!=i?E(ho(i),(function(e){_g(t.getItemGraphicEl(e),n,r)})):t.eachItemGraphicEl((function(t){_g(t,n,r)}))}function wg(t){return mg(t.model)}function Sg(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&vg(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),Mg[l]}Eo(xg),Go(xg);var Mg={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},Ig="\0__throttleOriginMethod",Tg="\0__throttleRate",Cg="\0__throttleType";function Dg(t,e,n){var i,r,o,a,s,l=0,u=0,h=null;function c(){u=(new Date).getTime(),h=null,t.apply(o,a||[])}e=e||0;var p=function(){for(var t=[],p=0;p=0?c():h=setTimeout(c,-r),l=i};return p.clear=function(){h&&(clearTimeout(h),h=null)},p.debounceNextCall=function(t){s=t},p}function Ag(t,e,n,i){var r=t[e];if(r){var o=r[Ig]||r,a=r[Cg];if(r[Tg]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=Dg(o,n,"debounce"===i))[Ig]=o,r[Cg]=i,r[Tg]=n}return r}}function kg(t,e){var n=t[e];n&&n[Ig]&&(n.clear&&n.clear(),t[e]=n[Ig])}var Lg=So(),Pg={itemStyle:Wo(hc,!0),lineStyle:Wo(sc,!0)},Og={lineStyle:"stroke",itemStyle:"fill"};function Rg(t,e){var n=t.visualStyleMapper||Pg[e];return n||(console.warn("Unkown style type '"+e+"'."),Pg.itemStyle)}function Ng(t,e){var n=t.visualDrawType||Og[e];return n||(console.warn("Unkown style type '"+e+"'."),"fill")}var Eg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=t.getModel(i),o=Rg(t,i)(r),a=r.getShallow("decal");a&&(n.setVisual("decal",a),a.dirty=!0);var s=Ng(t,i),l=o[s],u=U(l)?l:null,h="auto"===o.fill||"auto"===o.stroke;if(!o[s]||u||h){var c=t.getColorFromPalette(t.name,null,e.getSeriesCount());o[s]||(o[s]=c,n.setVisual("colorFromPalette",!0)),o.fill="auto"===o.fill||U(o.fill)?c:o.fill,o.stroke="auto"===o.stroke||U(o.stroke)?c:o.stroke}if(n.setVisual("style",o),n.setVisual("drawType",s),!e.isSeriesFiltered(t)&&u)return n.setVisual("colorFromPalette",!1),{dataEach:function(e,n){var i=t.getDataParams(n),r=A({},o);r[s]=u(i),e.setItemVisual(n,"style",r)}}}},zg=new dc,Vg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){if(!t.ignoreStyleOnData&&!e.isSeriesFiltered(t)){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=Rg(t,i),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(t,e){var n=t.getRawDataItem(e);if(n&&n[i]){zg.option=n[i];var a=r(zg);A(t.ensureUniqueItemVisual(e,"style"),a),zg.option.decal&&(t.setItemVisual(e,"decal",zg.option.decal),zg.option.decal.dirty=!0),o in a&&t.setItemVisual(e,"colorFromPalette",!1)}}:null}}}},Bg={performRawSeries:!0,overallReset:function(t){var e=ft();t.eachSeries((function(t){var n=t.getColorBy();if(!t.isColorBySeries()){var i=t.type+"-"+n,r=e.get(i);r||(r={},e.set(i,r)),Lg(t).scope=r}})),t.eachSeries((function(e){if(!e.isColorBySeries()&&!t.isSeriesFiltered(e)){var n=e.getRawData(),i={},r=e.getData(),o=Lg(e).scope,a=e.visualStyleAccessPath||"itemStyle",s=Ng(e,a);r.each((function(t){var e=r.getRawIndex(t);i[e]=t})),n.each((function(t){var a=i[t];if(r.getItemVisual(a,"colorFromPalette")){var l=r.ensureUniqueItemVisual(a,"style"),u=n.getName(t)||t+"",h=n.count();l[s]=e.getColorFromPalette(u,o,h)}}))}}))}},Fg=Math.PI;var Gg=function(){function t(t,e,n,i){this._stageTaskMap=ft(),this.ecInstance=t,this.api=e,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return t.prototype.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each((function(t){var e=t.overallTask;e&&e.dirty()}))},t.prototype.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},t.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},t.prototype.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.getData().count(),r=n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,o=t.get("large")&&i>=t.get("largeThreshold"),a="mod"===t.get("progressiveChunkMode")?i:null;t.pipelineContext=n.context={progressiveRender:r,modDataCount:a,large:o}},t.prototype.restorePipelines=function(t){var e=this,n=e._pipelineMap=ft();t.eachSeries((function(t){var i=t.getProgressive(),r=t.uid;n.set(r,{id:r,head:null,tail:null,threshold:t.getProgressiveThreshold(),progressiveEnabled:i&&!(t.preventIncremental&&t.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),e._pipe(t,t.dataTask)}))},t.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.api.getModel(),n=this.api;E(this._allHandlers,(function(i){var r=t.get(i.uid)||t.set(i.uid,{}),o="";lt(!(i.reset&&i.overallReset),o),i.reset&&this._createSeriesStageTask(i,r,e,n),i.overallReset&&this._createOverallStageTask(i,r,e,n)}),this)},t.prototype.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,this._pipe(e,r)},t.prototype.performDataProcessorTasks=function(t,e){this._performStageTasks(this._dataProcessorHandlers,t,e,{block:!0})},t.prototype.performVisualTasks=function(t,e,n){this._performStageTasks(this._visualHandlers,t,e,n)},t.prototype._performStageTasks=function(t,e,n,i){i=i||{};var r=!1,o=this;function a(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}E(t,(function(t,s){if(!i.visualType||i.visualType===t.visualType){var l=o._stageTaskMap.get(t.uid),u=l.seriesTaskMap,h=l.overallTask;if(h){var c,p=h.agentStubMap;p.each((function(t){a(i,t)&&(t.dirty(),c=!0)})),c&&h.dirty(),o.updatePayload(h,n);var d=o.getPerformArgs(h,i.block);p.each((function(t){t.perform(d)})),h.perform(d)&&(r=!0)}else u&&u.each((function(s,l){a(i,s)&&s.dirty();var u=o.getPerformArgs(s,i.block);u.skip=!t.performRawSeries&&e.isSeriesFiltered(s.context.model),o.updatePayload(s,n),s.perform(u)&&(r=!0)}))}})),this.unfinished=r||this.unfinished},t.prototype.performSeriesTasks=function(t){var e;t.eachSeries((function(t){e=t.dataTask.perform()||e})),this.unfinished=e||this.unfinished},t.prototype.plan=function(){this._pipelineMap.each((function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)}))},t.prototype.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},t.prototype._createSeriesStageTask=function(t,e,n,i){var r=this,o=e.seriesTaskMap,a=e.seriesTaskMap=ft(),s=t.seriesType,l=t.getTargetSeries;function u(e){var s=e.uid,l=a.set(s,o&&o.get(s)||hf({plan:Xg,reset:Zg,count:Kg}));l.context={model:e,ecModel:n,api:i,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:r},r._pipe(e,l)}t.createOnAllSeries?n.eachRawSeries(u):s?n.eachRawSeriesByType(s,u):l&&l(n,i).each(u)},t.prototype._createOverallStageTask=function(t,e,n,i){var r=this,o=e.overallTask=e.overallTask||hf({reset:Wg});o.context={ecModel:n,api:i,overallReset:t.overallReset,scheduler:r};var a=o.agentStubMap,s=o.agentStubMap=ft(),l=t.seriesType,u=t.getTargetSeries,h=!0,c=!1,p="";function d(t){var e=t.uid,n=s.set(e,a&&a.get(e)||(c=!0,hf({reset:Hg,onDirty:Ug})));n.context={model:t,overallProgress:h},n.agent=o,n.__block=h,r._pipe(t,n)}lt(!t.createOnAllSeries,p),l?n.eachRawSeriesByType(l,d):u?u(n,i).each(d):(h=!1,E(n.getSeries(),d)),c&&o.dirty()},t.prototype._pipe=function(t,e){var n=t.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=e),i.tail&&i.tail.pipe(e),i.tail=e,e.__idxInPipeline=i.count++,e.__pipeline=i},t.wrapStageHandler=function(t,e){return U(t)&&(t={overallReset:t,seriesType:$g(t)}),t.uid=gc("stageHandler"),e&&(t.visualType=e),t},t}();function Wg(t){t.overallReset(t.ecModel,t.api,t.payload)}function Hg(t){return t.overallProgress&&Yg}function Yg(){this.agent.dirty(),this.getDownstream().dirty()}function Ug(){this.agent&&this.agent.dirty()}function Xg(t){return t.plan?t.plan(t.model,t.ecModel,t.api,t.payload):null}function Zg(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=ho(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?z(e,(function(t,e){return qg(e)})):jg}var jg=qg(0);function qg(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o0&&h===r.length-u.length){var c=r.slice(0,h);"data"!==c&&(e.mainType=c,e[u.toLowerCase()]=t,s=!0)}}a.hasOwnProperty(r)&&(n[r]=t,s=!0),s||(i[r]=t)}))}return{cptQuery:e,dataQuery:n,otherQuery:i}},t.prototype.filter=function(t,e){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,r=n.packedEvent,o=n.model,a=n.view;if(!o||!a)return!0;var s=e.cptQuery,l=e.dataQuery;return u(s,o,"mainType")&&u(s,o,"subType")&&u(s,o,"index","componentIndex")&&u(s,o,"name")&&u(s,o,"id")&&u(l,r,"name")&&u(l,r,"dataIndex")&&u(l,r,"dataType")&&(!a.filterForExposedEvent||a.filterForExposedEvent(t,e.otherQuery,i,r));function u(t,e,n,i){return null==t[n]||e[i||n]===t[n]}},t.prototype.afterTrigger=function(){this.eventInfo=null},t}(),hy=["symbol","symbolSize","symbolRotate","symbolOffset"],cy=hy.concat(["symbolKeepAspect"]),py={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData();if(t.legendIcon&&n.setVisual("legendIcon",t.legendIcon),t.hasSymbolVisual){for(var i={},r={},o=!1,a=0;a=0&&Ry(l)?l:.5,t.createRadialGradient(a,s,0,a,s,l)}(t,e,n):function(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=Ry(i)?i:0,r=Ry(r)?r:1,o=Ry(o)?o:0,a=Ry(a)?a:0,t.createLinearGradient(i,o,r,a)}(t,e,n),r=e.colorStops,o=0;o0&&(e=i.lineDash,n=i.lineWidth,e&&"solid"!==e&&n>0?"dashed"===e?[4*n,2*n]:"dotted"===e?[n]:j(e)?[e]:Y(e)?e:null:null),o=i.lineDashOffset;if(r){var a=i.strokeNoScale&&t.getLineScale?t.getLineScale():1;a&&1!==a&&(r=z(r,(function(t){return t/a})),o/=a)}return[r,o]}var By=new qa(!0);function Fy(t){var e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))}function Gy(t){return"string"==typeof t&&"none"!==t}function Wy(t){var e=t.fill;return null!=e&&"none"!==e}function Hy(t,e){if(null!=e.fillOpacity&&1!==e.fillOpacity){var n=t.globalAlpha;t.globalAlpha=e.fillOpacity*e.opacity,t.fill(),t.globalAlpha=n}else t.fill()}function Yy(t,e){if(null!=e.strokeOpacity&&1!==e.strokeOpacity){var n=t.globalAlpha;t.globalAlpha=e.strokeOpacity*e.opacity,t.stroke(),t.globalAlpha=n}else t.stroke()}function Uy(t,e,n){var i=Zo(e.image,e.__image,n);if(qo(i)){var r=t.createPattern(i,e.repeat||"repeat");if("function"==typeof DOMMatrix&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(e.x||0,e.y||0),o.rotateSelf(0,0,(e.rotation||0)*_t),o.scaleSelf(e.scaleX||1,e.scaleY||1),r.setTransform(o)}return r}}var Xy=["shadowBlur","shadowOffsetX","shadowOffsetY"],Zy=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function jy(t,e,n,i,r){var o=!1;if(!i&&e===(n=n||{}))return!1;if(i||e.opacity!==n.opacity){$y(t,r),o=!0;var a=Math.max(Math.min(e.opacity,1),0);t.globalAlpha=isNaN(a)?ua.opacity:a}(i||e.blend!==n.blend)&&(o||($y(t,r),o=!0),t.globalCompositeOperation=e.blend||ua.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,e,n){if(!this.__flagInMainProcess)if(this._disposed)Hv(this.id);else{var i,r,o;if(q(e)&&(n=e.lazyUpdate,i=e.silent,r=e.replaceMerge,o=e.transition,e=e.notMerge),this.__flagInMainProcess=!0,!this._model||e){var a=new pd(this._api),s=this._theme,l=this._model=new id;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,a)}this._model.setOption(t,{replaceMerge:r},Zv);var u={seriesTransition:o,optionChanged:!0};if(n)this.__pendingUpdate={silent:i,updateParams:u},this.__flagInMainProcess=!1,this.getZr().wakeUp();else{try{_v(this),Sv.update.call(this,null,u)}catch(t){throw this.__pendingUpdate=null,this.__flagInMainProcess=!1,t}this._ssr||this._zr.flush(),this.__pendingUpdate=null,this.__flagInMainProcess=!1,Cv.call(this,i),Dv.call(this,i)}}},e.prototype.setTheme=function(){oo()},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||cv&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var e=this._zr.painter;return e.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var e=this._zr.painter;return e.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){if(r.svgSupported){var t=this._zr;return E(t.storage.getDisplayList(),(function(t){t.stopAnimation(null,!0)})),t.painter.toDataURL()}},e.prototype.getDataURL=function(t){if(!this._disposed){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;E(e,(function(t){n.eachComponent({mainType:t},(function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)}))}));var o="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return E(i,(function(t){t.group.ignore=!1})),o}Hv(this.id)},e.prototype.getConnectedDataURL=function(t){if(!this._disposed){var e="svg"===t.type,n=this.group,i=Math.min,r=Math.max,o=1/0;if(Jv[n]){var a=o,s=o,l=-1/0,u=-1/0,c=[],p=t&&t.pixelRatio||this.getDevicePixelRatio();E($v,(function(o,h){if(o.group===n){var p=e?o.getZr().painter.getSvgDom().innerHTML:o.renderToCanvas(T(t)),d=o.getDom().getBoundingClientRect();a=i(d.left,a),s=i(d.top,s),l=r(d.right,l),u=r(d.bottom,u),c.push({dom:p,left:d.left,top:d.top})}}));var d=(l*=p)-(a*=p),f=(u*=p)-(s*=p),g=h.createCanvas(),y=Lr(g,{renderer:e?"svg":"canvas"});if(y.resize({width:d,height:f}),e){var v="";return E(c,(function(t){var e=t.left-a,n=t.top-s;v+=''+t.dom+""})),y.painter.getSvgRoot().innerHTML=v,t.connectedBackgroundColor&&y.painter.setBackgroundColor(t.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}return t.connectedBackgroundColor&&y.add(new Cs({shape:{x:0,y:0,width:d,height:f},style:{fill:t.connectedBackgroundColor}})),E(c,(function(t){var e=new _s({style:{x:t.left*p-a,y:t.top*p-s,image:t.dom}});y.add(e)})),y.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}Hv(this.id)},e.prototype.convertToPixel=function(t,e){return Mv(this,"convertToPixel",t,e)},e.prototype.convertFromPixel=function(t,e){return Mv(this,"convertFromPixel",t,e)},e.prototype.containPixel=function(t,e){var n;if(!this._disposed)return E(Io(this._model,t),(function(t,i){i.indexOf("Models")>=0&&E(t,(function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n=n||!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n=n||o.containPoint(e,t))}else 0}),this)}),this),!!n;Hv(this.id)},e.prototype.getVisual=function(t,e){var n=Io(this._model,t,{defaultMainType:"series"}),i=n.seriesModel;var r=i.getData(),o=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?r.indexOfRawIndex(n.dataIndex):null;return null!=o?fy(r,o,e):gy(r,e)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t,e,n,i=this;E(Wv,(function(t){var e=function(e){var n,r=i.getModel(),o=e.target,a="globalout"===t;if(a?n={}:o&&xy(o,(function(t){var e=Hs(t);if(e&&null!=e.dataIndex){var i=e.dataModel||r.getSeriesByIndex(e.seriesIndex);return n=i&&i.getDataParams(e.dataIndex,e.dataType)||{},!0}if(e.eventData)return n=A({},e.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var u=s&&null!=l&&r.getComponent(s,l),h=u&&i["series"===u.mainType?"_chartsMap":"_componentsMap"][u.__viewId];0,n.event=e,n.type=t,i._$eventProcessor.eventInfo={targetEl:o,packedEvent:n,model:u,view:h},i.trigger(t,n)}};e.zrEventfulCallAtLast=!0,i._zr.on(t,e,i)})),E(Uv,(function(t,e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),E(["selectchanged"],(function(t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),t=this._messageCenter,e=this,n=this._api,t.on("selectchanged",(function(t){var i=n.getModel();t.isFromClick?(my("map","selectchanged",e,i,t),my("pie","selectchanged",e,i,t)):"select"===t.fromAction?(my("map","selected",e,i,t),my("pie","selected",e,i,t)):"unselect"===t.fromAction&&(my("map","unselected",e,i,t),my("pie","unselected",e,i,t))}))},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){this._disposed?Hv(this.id):this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed)Hv(this.id);else{this._disposed=!0,this.getDom()&&ko(this.getDom(),em,"");var t=this,e=t._api,n=t._model;E(t._componentsViews,(function(t){t.dispose(n,e)})),E(t._chartsViews,(function(t){t.dispose(n,e)})),t._zr.dispose(),t._dom=t._model=t._chartsMap=t._componentsMap=t._chartsViews=t._componentsViews=t._scheduler=t._api=t._zr=t._throttledZrFlush=t._theme=t._coordSysMgr=t._messageCenter=null,delete $v[t.id]}},e.prototype.resize=function(t){if(!this.__flagInMainProcess)if(this._disposed)Hv(this.id);else{this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this.__pendingUpdate&&(null==i&&(i=this.__pendingUpdate.silent),n=!0,this.__pendingUpdate=null),this.__flagInMainProcess=!0;try{n&&_v(this),Sv.update.call(this,{type:"resize",animation:A({duration:0},t&&t.animation)})}catch(t){throw this.__flagInMainProcess=!1,t}this.__flagInMainProcess=!1,Cv.call(this,i),Dv.call(this,i)}}},e.prototype.showLoading=function(t,e){if(this._disposed)Hv(this.id);else if(q(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),Kv[t]){var n=Kv[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){this._disposed?Hv(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},e.prototype.makeActionFromEvent=function(t){var e=A({},t);return e.type=Uv[t.type],e},e.prototype.dispatchAction=function(t,e){if(this._disposed)Hv(this.id);else if(q(e)||(e={silent:!!e}),Yv[t.type]&&this._model)if(this.__flagInMainProcess)this._pendingActions.push(t);else{var n=e.silent;Tv.call(this,t,n);var i=e.flush;i?this._zr.flush():!1!==i&&r.browser.weChat&&this._throttledZrFlush(),Cv.call(this,n),Dv.call(this,n)}},e.prototype.updateLabelLayout=function(){lv.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed)Hv(this.id);else{var e=t.seriesIndex,n=this.getModel().getSeriesByIndex(e);0,n.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},e.internalField=function(){function t(t){t.clearColorPalette(),t.eachSeries((function(t){t.clearColorPalette()}))}function e(t){for(var e=[],n=t.currentStates,i=0;i0?{duration:o,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(t){if(t.states&&t.states.emphasis){if(ah(t))return;if(t instanceof gs&&function(t){var e=Zs(t);e.normalFill=t.style.fill,e.normalStroke=t.style.stroke;var n=t.states.select||{};e.selectFill=n.style&&n.style.fill||null,e.selectStroke=n.style&&n.style.stroke||null}(t),t.__dirty){var n=t.prevStates;n&&t.useStates(n)}if(r){t.stateTransition=a;var i=t.getTextContent(),o=t.getTextGuideLine();i&&(i.stateTransition=a),o&&(o.stateTransition=a)}t.__dirty&&e(t)}}))}_v=function(t){var e=t._scheduler;e.restorePipelines(t._model),e.prepareStageTasks(),bv(t,!0),bv(t,!1),e.plan()},bv=function(t,e){for(var n=t._model,i=t._scheduler,r=e?t._componentsViews:t._chartsViews,o=e?t._componentsMap:t._chartsMap,a=t._zr,s=t._api,l=0;le.get("hoverLayerThreshold")&&!r.node&&!r.worker&&e.eachSeries((function(e){if(!e.preventUsingHoverLayer){var n=t._chartsMap[e.__viewId];n.__alive&&n.eachRendered((function(t){t.states.emphasis&&(t.states.emphasis.hoverLayer=!0)}))}}))}(t,e),lv.trigger("series:afterupdate",e,n,l)},Ev=function(t){t.__needsUpdateStatus=!0,t.getZr().wakeUp()},zv=function(t){t.__needsUpdateStatus&&(t.getZr().storage.traverse((function(t){ah(t)||e(t)})),t.__needsUpdateStatus=!1)},Rv=function(t){return new(function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return n(i,e),i.prototype.getCoordinateSystems=function(){return t._coordSysMgr.getCoordinateSystems()},i.prototype.getComponentByElement=function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}},i.prototype.enterEmphasis=function(e,n){_l(e,n),Ev(t)},i.prototype.leaveEmphasis=function(e,n){bl(e,n),Ev(t)},i.prototype.enterBlur=function(e){wl(e),Ev(t)},i.prototype.leaveBlur=function(e){Sl(e),Ev(t)},i.prototype.enterSelect=function(e){Ml(e),Ev(t)},i.prototype.leaveSelect=function(e){Il(e),Ev(t)},i.prototype.getModel=function(){return t.getModel()},i.prototype.getViewOfComponentModel=function(e){return t.getViewOfComponentModel(e)},i.prototype.getViewOfSeriesModel=function(e){return t.getViewOfSeriesModel(e)},i}(ld))(t)},Nv=function(t){function e(t,e){for(var n=0;n=0)){gm.push(n);var o=Gg.wrapStageHandler(n,r);o.__prio=e,o.__raw=n,t.push(o)}}function vm(t,e){Kv[t]=e}function mm(t,e,n){var i=hv("registerMap");i&&i(t,e,n)}var xm=function(t){var e=(t=T(t)).type,n="";e||ao(n);var i=e.split(":");2!==i.length&&ao(n);var r=!1;"echarts"===i[0]&&(e=i[1],r=!0),t.__isBuiltIn=r,Tf.set(e,t)};fm(pv,Eg),fm(dv,Vg),fm(dv,Bg),fm(pv,py),fm(dv,dy),fm(7e3,(function(t,e){t.eachRawSeries((function(n){if(!t.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(t){var n=i.getItemVisual(t,"decal");n&&(i.ensureUniqueItemVisual(t,"style").decal=rv(n,e))}));var r=i.getVisual("decal");if(r)i.getVisual("style").decal=rv(r,e)}}))})),am(Od),sm(900,(function(t){var e=ft();t.eachSeries((function(t){var n=t.get("stack");if(n){var i=e.get(n)||e.set(n,[]),r=t.getData(),o={stackResultDimension:r.getCalculationInfo("stackResultDimension"),stackedOverDimension:r.getCalculationInfo("stackedOverDimension"),stackedDimension:r.getCalculationInfo("stackedDimension"),stackedByDimension:r.getCalculationInfo("stackedByDimension"),isStackedByIndex:r.getCalculationInfo("isStackedByIndex"),data:r,seriesModel:t};if(!o.stackedDimension||!o.isStackedByIndex&&!o.stackedByDimension)return;i.length&&r.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(o)}})),e.each(Rd)})),vm("default",(function(t,e){k(e=e||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new Cr,i=new Cs({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});n.add(i);var r,o=new ks({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),a=new Cs({style:{fill:"none"},textContent:o,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});return n.add(a),e.showSpinner&&((r=new Hu({shape:{startAngle:-Fg/2,endAngle:-Fg/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*Fg/2}).start("circularInOut"),r.animateShape(!0).when(1e3,{startAngle:3*Fg/2}).delay(300).start("circularInOut"),n.add(r)),n.resize=function(){var n=o.getBoundingRect().width,s=e.showSpinner?e.spinnerRadius:0,l=(t.getWidth()-2*s-(e.showSpinner&&n?10:0)-n)/2-(e.showSpinner&&n?0:5+n/2)+(e.showSpinner?0:n/2)+(n?0:s),u=t.getHeight()/2;e.showSpinner&&r.setShape({cx:l,cy:u}),a.setShape({x:l-s,y:u-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},n.resize(),n})),cm({type:Js,event:Js,update:Js},xt),cm({type:Qs,event:Qs,update:Qs},xt),cm({type:tl,event:tl,update:tl},xt),cm({type:el,event:el,update:el},xt),cm({type:nl,event:nl,update:nl},xt),om("light",iy),om("dark",ly);var _m=[],bm={registerPreprocessor:am,registerProcessor:sm,registerPostInit:lm,registerPostUpdate:um,registerUpdateLifecycle:hm,registerAction:cm,registerCoordinateSystem:pm,registerLayout:dm,registerVisual:fm,registerTransform:xm,registerLoading:vm,registerMap:mm,registerImpl:function(t,e){uv[t]=e},PRIORITY:fv,ComponentModel:Tp,ComponentView:gg,SeriesModel:sg,ChartView:xg,registerComponentModel:function(t){Tp.registerClass(t)},registerComponentView:function(t){gg.registerClass(t)},registerSeriesModel:function(t){sg.registerClass(t)},registerChartView:function(t){xg.registerClass(t)},registerSubTypeDefaulter:function(t,e){Tp.registerSubTypeDefaulter(t,e)},registerPainter:function(t,e){Pr(t,e)}};function wm(t){Y(t)?E(t,(function(t){wm(t)})):P(_m,t)>=0||(_m.push(t),U(t)&&(t={install:t}),t.install(bm))}function Sm(t){return null==t?0:t.length||1}function Mm(t){return t}var Im=function(){function t(t,e,n,i,r,o){this._old=t,this._new=e,this._oldKeyGetter=n||Mm,this._newKeyGetter=i||Mm,this.context=r,this._diffModeMultiple="multiple"===o}return t.prototype.add=function(t){return this._add=t,this},t.prototype.update=function(t){return this._update=t,this},t.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},t.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},t.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},t.prototype.remove=function(t){return this._remove=t,this},t.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},t.prototype._executeOneToOne=function(){var t=this._old,e=this._new,n={},i=new Array(t.length),r=new Array(e.length);this._initIndexMap(t,null,i,"_oldKeyGetter"),this._initIndexMap(e,n,r,"_newKeyGetter");for(var o=0;o1){var u=s.shift();1===s.length&&(n[a]=s[0]),this._update&&this._update(u,o)}else 1===l?(n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o)}this._performRestAdd(r,n)},t.prototype._executeMultiple=function(){var t=this._old,e=this._new,n={},i={},r=[],o=[];this._initIndexMap(t,n,r,"_oldKeyGetter"),this._initIndexMap(e,i,o,"_newKeyGetter");for(var a=0;a1&&1===c)this._updateManyToOne&&this._updateManyToOne(u,l),i[s]=null;else if(1===h&&c>1)this._updateOneToMany&&this._updateOneToMany(u,l),i[s]=null;else if(1===h&&1===c)this._update&&this._update(u,l),i[s]=null;else if(h>1&&c>1)this._updateManyToMany&&this._updateManyToMany(u,l),i[s]=null;else if(h>1)for(var p=0;p1)for(var a=0;a30}var zm,Vm,Bm,Fm,Gm,Wm,Hm,Ym=q,Um=z,Xm="undefined"==typeof Int32Array?Array:Int32Array,Zm=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],jm=["_approximateExtent"],qm=function(){function t(t,e){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;Om(t)?(n=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(i=!0,n=t),n=n||["x","y"];for(var r={},o=[],a={},s=!1,l={},u=0;u=e)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,r=this._idList;if(n.getSource().sourceFormat===Lp&&!n.pure)for(var o=[],a=t;a0},t.prototype.ensureUniqueItemVisual=function(t,e){var n=this._itemVisuals,i=n[t];i||(i=n[t]={});var r=i[e];return null==r&&(Y(r=this.getVisual(e))?r=r.slice():Ym(r)&&(r=A({},r)),i[e]=r),r},t.prototype.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{};this._itemVisuals[t]=i,Ym(e)?A(i,e):i[e]=n},t.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},t.prototype.setLayout=function(t,e){Ym(t)?A(this._layout,t):this._layout[t]=e},t.prototype.getLayout=function(t){return this._layout[t]},t.prototype.getItemLayout=function(t){return this._itemLayouts[t]},t.prototype.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?A(this._itemLayouts[t]||{},e):e},t.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},t.prototype.setItemGraphicEl=function(t,e){var n=this.hostModel&&this.hostModel.seriesIndex;Ys(n,this.dataType,t,e),this._graphicEls[t]=e},t.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},t.prototype.eachItemGraphicEl=function(t,e){E(this._graphicEls,(function(n,i){n&&t&&t.call(e,n,i)}))},t.prototype.cloneShallow=function(e){return e||(e=new t(this._schema?this._schema:Um(this.dimensions,this._getDimInfo,this),this.hostModel)),Gm(e,this),e._store=this._store,e},t.prototype.wrapMethod=function(t,e){var n=this[t];U(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(at(arguments)))})},t.internalField=(zm=function(t){var e=t._invertedIndicesMap;E(e,(function(n,i){var r=t._dimInfos[i],o=r.ordinalMeta,a=t._store;if(o){n=e[i]=new Xm(o.categories.length);for(var s=0;s1&&(s+="__ec__"+u),i[e]=s}})),t}();function Km(t,e){Gd(t)||(t=Hd(t));var n=(e=e||{}).coordDimensions||[],i=e.dimensionsDefine||t.dimensionsDefine||[],r=ft(),o=[],a=function(t,e,n,i){var r=Math.max(t.dimensionsDetectedCount||1,e.length,n.length,i||0);return E(e,(function(t){var e;q(t)&&(e=t.dimsDef)&&(r=Math.max(r,e.length))})),r}(t,n,i,e.dimensionsCount),s=e.canOmitUnusedDimensions&&Em(a),l=i===t.dimensionsDefine,u=l?Nm(t):Rm(i),h=e.encodeDefine;!h&&e.encodeDefaulter&&(h=e.encodeDefaulter(t,a));for(var c=ft(h),p=new Of(a),d=0;d0&&(i.name=r+(o-1)),o++,e.set(r,o)}}(o),new Pm({source:t,dimensions:o,fullDimensionCount:a,dimensionOmitted:s})}function $m(t,e,n){var i=e.data;if(n||i.hasOwnProperty(t)){for(var r=0;i.hasOwnProperty(t+r);)r++;t+=r}return e.set(t,!0),t}var Jm=function(t){this.coordSysDims=[],this.axisMap=ft(),this.categoryAxisMap=ft(),this.coordSysName=t};var Qm={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis",Co).models[0],o=t.getReferringComponents("yAxis",Co).models[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),tx(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),tx(o)&&(i.set("y",o),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis",Co).models[0];e.coordSysDims=["single"],n.set("single",r),tx(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar",Co).models[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),tx(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),tx(a)&&(i.set("angle",a),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();E(o.parallelAxisIndex,(function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),tx(s)&&(i.set(l,s),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=o))}))}};function tx(t){return"category"===t.get("type")}function ex(t,e,n){var i,r,o,a=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(t){return!Om(t.schema)}(e)?(r=e.schema,i=r.dimensions,o=e.store):i=e;var l,u,h,c,p=!(!t||!t.get("stack"));if(E(i,(function(t,e){X(t)&&(i[e]=t={name:t}),p&&!t.isExtraCoord&&(a||l||!t.ordinalMeta||(l=t),u||"ordinal"===t.type||"time"===t.type||s&&s!==t.coordDim||(u=t))})),!u||a||l||(a=!0),u){h="__\0ecstackresult_"+t.id,c="__\0ecstackedover_"+t.id,l&&(l.createInvertedIndices=!0);var d=u.coordDim,f=u.type,g=0;E(i,(function(t){t.coordDim===d&&g++}));var y={name:h,coordDim:d,coordDimIndex:g,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},v={name:c,coordDim:c,coordDimIndex:g+1,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};r?(o&&(y.storeDimIndex=o.ensureCalculationDimension(c,f),v.storeDimIndex=o.ensureCalculationDimension(h,f)),r.appendCalculationDimension(y),r.appendCalculationDimension(v)):(i.push(y),i.push(v))}return{stackedDimension:u&&u.name,stackedByDimension:l&&l.name,isStackedByIndex:a,stackedOverDimension:c,stackResultDimension:h}}function nx(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function ix(t,e){return nx(t,e)?t.getCalculationInfo("stackResultDimension"):e}function rx(t,e,n){n=n||{};var i,r=e.getSourceManager(),o=!1;t?(o=!0,i=Hd(t)):o=(i=r.getSource()).sourceFormat===Lp;var a=function(t){var e=t.get("coordinateSystem"),n=new Jm(e),i=Qm[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}(e),s=function(t,e){var n,i=t.get("coordinateSystem"),r=hd.get(i);return e&&e.coordSysDims&&(n=z(e.coordSysDims,(function(t){var n={name:t},i=e.axisMap.get(t);if(i){var r=i.get("type");n.type=Dm(r)}return n}))),n||(n=r&&(r.getDimensionsInfo?r.getDimensionsInfo():r.dimensions.slice())||["x","y"]),n}(e,a),l=n.useEncodeDefaulter,u=U(l)?l:l?H(Hp,s,e):null,h=Km(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o}),c=function(t,e,n){var i,r;return n&&E(t,(function(t,o){var a=t.coordDim,s=n.categoryAxisMap.get(a);s&&(null==i&&(i=o),t.ordinalMeta=s.getOrdinalMeta(),e&&(t.createInvertedIndices=!0)),null!=t.otherDims.itemName&&(r=!0)})),r||null==i||(t[i].otherDims.itemName=0),i}(h.dimensions,n.createInvertedIndices,a),p=o?null:r.getSharedDataStore(h),d=ex(e,{schema:h,store:p}),f=new qm(h,e);f.setCalculationInfo(d);var g=null!=c&&function(t){if(t.sourceFormat===Lp){return!Y(fo(function(t){var e=0;for(;ee[1]&&(e[1]=t[1])},t.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=t),isNaN(e)||(n[1]=e)},t.prototype.isInExtentRange=function(t){return this._extent[0]<=t&&this._extent[1]>=t},t.prototype.isBlank=function(){return this._isBlank},t.prototype.setBlank=function(t){this._isBlank=t},t}();Go(ox);var ax=0,sx=function(){function t(t){this.categories=t.categories||[],this._needCollect=t.needCollect,this._deduplication=t.deduplication,this.uid=++ax}return t.createByAxisModel=function(e){var n=e.option,i=n.data,r=i&&z(i,lx);return new t({categories:r,needCollect:!r,deduplication:!1!==n.dedplication})},t.prototype.getOrdinal=function(t){return this._getOrCreateMap().get(t)},t.prototype.parseAndCollect=function(t){var e,n=this._needCollect;if(!X(t)&&!n)return t;if(n&&!this._deduplication)return e=this.categories.length,this.categories[e]=t,e;var i=this._getOrCreateMap();return null==(e=i.get(t))&&(n?(e=this.categories.length,this.categories[e]=t,i.set(t,e)):e=NaN),e},t.prototype._getOrCreateMap=function(){return this._map||(this._map=ft(this.categories))},t}();function lx(t){return q(t)&&null!=t.value?t.value:t+""}function ux(t){return"interval"===t.type||"log"===t.type}function hx(t,e,n,i){var r={},o=t[1]-t[0],a=r.interval=$r(o/e,!0);null!=n&&ai&&(a=r.interval=i);var s=r.intervalPrecision=px(a);return function(t,e){!isFinite(t[0])&&(t[0]=e[0]),!isFinite(t[1])&&(t[1]=e[1]),dx(t,0,e),dx(t,1,e),t[0]>t[1]&&(t[0]=t[1])}(r.niceTickExtent=[zr(Math.ceil(t[0]/a)*a,s),zr(Math.floor(t[1]/a)*a,s)],t),r}function cx(t){var e=Math.pow(10,Kr(t)),n=t/e;return n?2===n?n=3:3===n?n=5:n*=2:n=1,zr(n*e)}function px(t){return Br(t)+2}function dx(t,e,n){t[e]=Math.max(Math.min(t[e],n[1]),n[0])}function fx(t,e){return t>=e[0]&&t<=e[1]}function gx(t,e){return e[1]===e[0]?.5:(t-e[0])/(e[1]-e[0])}function yx(t,e){return t*(e[1]-e[0])+e[0]}var vx=function(t){function e(e){var n=t.call(this,e)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new sx({})),Y(i)&&(i=new sx({categories:z(i,(function(t){return q(t)?t.value:t}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return n(e,t),e.prototype.parse=function(t){return null==t?NaN:X(t)?this._ordinalMeta.getOrdinal(t):Math.round(t)},e.prototype.contain=function(t){return fx(t=this.parse(t),this._extent)&&null!=this._ordinalMeta.categories[t]},e.prototype.normalize=function(t){return gx(t=this._getTickNumber(this.parse(t)),this._extent)},e.prototype.scale=function(t){return t=Math.round(yx(t,this._extent)),this.getRawOrdinalNumber(t)},e.prototype.getTicks=function(){for(var t=[],e=this._extent,n=e[0];n<=e[1];)t.push({value:n}),n++;return t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(null!=t){for(var e=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],r=0,o=this._ordinalMeta.categories.length,a=Math.min(o,e.length);r=0&&t=0&&t=t},e.prototype.getOrdinalMeta=function(){return this._ordinalMeta},e.prototype.calcNiceTicks=function(){},e.prototype.calcNiceExtent=function(){},e.type="ordinal",e}(ox);ox.registerClass(vx);var mx=zr,xx=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="interval",e._interval=0,e._intervalPrecision=2,e}return n(e,t),e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return fx(t,this._extent)},e.prototype.normalize=function(t){return gx(t,this._extent)},e.prototype.scale=function(t){return yx(t,this._extent)},e.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(e)||(n[1]=parseFloat(e))},e.prototype.unionExtent=function(t){var e=this._extent;t[0]e[1]&&(e[1]=t[1]),this.setExtent(e[0],e[1])},e.prototype.getInterval=function(){return this._interval},e.prototype.setInterval=function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=px(t)},e.prototype.getTicks=function(t){var e=this._interval,n=this._extent,i=this._niceExtent,r=this._intervalPrecision,o=[];if(!e)return o;n[0]1e4)return[];var s=o.length?o[o.length-1].value:i[1];return n[1]>s&&(t?o.push({value:mx(s+e,r)}):o.push({value:n[1]})),o},e.prototype.getMinorTicks=function(t){for(var e=this.getTicks(!0),n=[],i=this.getExtent(),r=1;ri[0]&&h0&&(o=null===o?s:Math.min(o,s))}n[i]=o}}return n}(t),n=[];return E(t,(function(t){var i,r=t.coordinateSystem.getBaseAxis(),o=r.getExtent();if("category"===r.type)i=r.getBandWidth();else if("value"===r.type||"time"===r.type){var a=r.dim+"_"+r.index,s=e[a],l=Math.abs(o[1]-o[0]),u=r.scale.getExtent(),h=Math.abs(u[1]-u[0]);i=s?l/h*s:l}else{var c=t.getData();i=Math.abs(o[1]-o[0])/c.count()}var p=Er(t.get("barWidth"),i),d=Er(t.get("barMaxWidth"),i),f=Er(t.get("barMinWidth")||(Ox(t)?.5:1),i),g=t.get("barGap"),y=t.get("barCategoryGap");n.push({bandWidth:i,barWidth:p,barMaxWidth:d,barMinWidth:f,barGap:g,barCategoryGap:y,axisKey:Tx(r),stackId:Ix(t)})})),Ax(n)}function Ax(t){var e={};E(t,(function(t,n){var i=t.axisKey,r=t.bandWidth,o=e[i]||{bandWidth:r,remainedWidth:r,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},a=o.stacks;e[i]=o;var s=t.stackId;a[s]||o.autoWidthCount++,a[s]=a[s]||{width:0,maxWidth:0};var l=t.barWidth;l&&!a[s].width&&(a[s].width=l,l=Math.min(o.remainedWidth,l),o.remainedWidth-=l);var u=t.barMaxWidth;u&&(a[s].maxWidth=u);var h=t.barMinWidth;h&&(a[s].minWidth=h);var c=t.barGap;null!=c&&(o.gap=c);var p=t.barCategoryGap;null!=p&&(o.categoryGap=p)}));var n={};return E(e,(function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=t.categoryGap;if(null==o){var a=G(i).length;o=Math.max(35-4*a,15)+"%"}var s=Er(o,r),l=Er(t.gap,1),u=t.remainedWidth,h=t.autoWidthCount,c=(u-s)/(h+(h-1)*l);c=Math.max(c,0),E(i,(function(t){var e=t.maxWidth,n=t.minWidth;if(t.width){i=t.width;e&&(i=Math.min(i,e)),n&&(i=Math.max(i,n)),t.width=i,u-=i+l*i,h--}else{var i=c;e&&ei&&(i=n),i!==c&&(t.width=i,u-=i+l*i,h--)}})),c=(u-s)/(h+(h-1)*l),c=Math.max(c,0);var p,d=0;E(i,(function(t,e){t.width||(t.width=c),p=t,d+=t.width*(1+l)})),p&&(d-=p.width*l);var f=-d/2;E(i,(function(t,i){n[e][i]=n[e][i]||{bandWidth:r,offset:f,width:t.width},f+=t.width*(1+l)}))})),n}function kx(t,e){var n=Cx(t,e),i=Dx(n);E(n,(function(t){var e=t.getData(),n=t.coordinateSystem.getBaseAxis(),r=Ix(t),o=i[Tx(n)][r],a=o.offset,s=o.width;e.setLayout({bandWidth:o.bandWidth,offset:a,size:s})}))}function Lx(t){return{seriesType:t,plan:yg(),reset:function(t){if(Px(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.getDimensionIndex(e.mapDimension(r.dim)),a=e.getDimensionIndex(e.mapDimension(i.dim)),s=t.get("showBackground",!0),l=e.mapDimension(r.dim),u=e.getCalculationInfo("stackResultDimension"),h=nx(e,l)&&!!e.getCalculationInfo("stackedOnSeries"),c=r.isHorizontal(),p=function(t,e){return e.toGlobalCoord(e.dataToCoord("log"===e.type?1:0))}(0,r),d=Ox(t),f=t.get("barMinHeight")||0,g=u&&e.getDimensionIndex(u),y=e.getLayout("size"),v=e.getLayout("offset");return{progress:function(t,e){for(var i,r=t.count,l=d&&Sx(3*r),u=d&&s&&Sx(3*r),m=d&&Sx(r),x=n.master.getRect(),_=c?x.width:x.height,b=e.getStore(),w=0;null!=(i=t.next());){var S=b.get(h?g:o,i),M=b.get(a,i),I=p,T=void 0;h&&(T=+S-b.get(o,i));var C=void 0,D=void 0,A=void 0,k=void 0;if(c){var L=n.dataToPoint([S,M]);if(h)I=n.dataToPoint([T,M])[0];C=I,D=L[1]+v,A=L[0]-I,k=y,Math.abs(A)0)for(var s=0;s=0;--s)if(l[u]){o=l[u];break}o=o||a.none}if(Y(o)){var h=null==t.level?0:t.level>=0?t.level:o.length+t.level;o=o[h=Math.min(h,o.length-1)]}}return Vc(new Date(t.value),o,r,i)}(t,e,n,this.getSetting("locale"),i)},e.prototype.getTicks=function(){var t=this._interval,e=this._extent,n=[];if(!t)return n;n.push({value:e[0],level:0});var i=this.getSetting("useUTC"),r=function(t,e,n,i){var r=1e4,o=Rc,a=0;function s(t,e,n,r,o,a,s){for(var l=new Date(e),u=e,h=l[r]();u1&&0===u&&o.unshift({value:o[0].value-p})}}for(u=0;u=i[0]&&v<=i[1]&&c++)}var m=(i[1]-i[0])/e;if(c>1.5*m&&p>m/1.5)break;if(u.push(g),c>m||t===o[d])break}h=[]}}0;var x=B(z(u,(function(t){return B(t,(function(t){return t.value>=i[0]&&t.value<=i[1]&&!t.notAdd}))})),(function(t){return t.length>0})),_=[],b=x.length-1;for(d=0;dn&&(this._approxInterval=n);var o=Nx.length,a=Math.min(function(t,e,n,i){for(;n>>1;t[r][1]16?16:t>7.5?7:t>3.5?4:t>1.5?2:1}function zx(t){return(t/=2592e6)>6?6:t>3?3:t>2?2:1}function Vx(t){return(t/=Cc)>12?12:t>6?6:t>3.5?4:t>2?2:1}function Bx(t,e){return(t/=e?Tc:Ic)>30?30:t>20?20:t>15?15:t>10?10:t>5?5:t>2?2:1}function Fx(t){return $r(t,!0)}function Gx(t,e,n){var i=new Date(t);switch(Ec(e)){case"year":case"month":i[qc(n)](0);case"day":i[Kc(n)](1);case"hour":i[$c(n)](0);case"minute":i[Jc(n)](0);case"second":i[Qc(n)](0),i[tp(n)](0)}return i.getTime()}ox.registerClass(Rx);var Wx=ox.prototype,Hx=xx.prototype,Yx=zr,Ux=Math.floor,Xx=Math.ceil,Zx=Math.pow,jx=Math.log,qx=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="log",e.base=10,e._originalScale=new xx,e._interval=0,e}return n(e,t),e.prototype.getTicks=function(t){var e=this._originalScale,n=this._extent,i=e.getExtent();return z(Hx.getTicks.call(this,t),(function(t){var e=t.value,r=zr(Zx(this.base,e));return r=e===n[0]&&this._fixMin?$x(r,i[0]):r,{value:r=e===n[1]&&this._fixMax?$x(r,i[1]):r}}),this)},e.prototype.setExtent=function(t,e){var n=this.base;t=jx(t)/jx(n),e=jx(e)/jx(n),Hx.setExtent.call(this,t,e)},e.prototype.getExtent=function(){var t=this.base,e=Wx.getExtent.call(this);e[0]=Zx(t,e[0]),e[1]=Zx(t,e[1]);var n=this._originalScale.getExtent();return this._fixMin&&(e[0]=$x(e[0],n[0])),this._fixMax&&(e[1]=$x(e[1],n[1])),e},e.prototype.unionExtent=function(t){this._originalScale.unionExtent(t);var e=this.base;t[0]=jx(t[0])/jx(e),t[1]=jx(t[1])/jx(e),Wx.unionExtent.call(this,t)},e.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},e.prototype.calcNiceTicks=function(t){t=t||10;var e=this._extent,n=e[1]-e[0];if(!(n===1/0||n<=0)){var i=qr(n);for(t/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var r=[zr(Xx(e[0]/i)*i),zr(Ux(e[1]/i)*i)];this._interval=i,this._niceExtent=r}},e.prototype.calcNiceExtent=function(t){Hx.calcNiceExtent.call(this,t),this._fixMin=t.fixMin,this._fixMax=t.fixMax},e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return fx(t=jx(t)/jx(this.base),this._extent)},e.prototype.normalize=function(t){return gx(t=jx(t)/jx(this.base),this._extent)},e.prototype.scale=function(t){return t=yx(t,this._extent),Zx(this.base,t)},e.type="log",e}(ox),Kx=qx.prototype;function $x(t,e){return Yx(t,Br(e))}Kx.getMinorTicks=Hx.getMinorTicks,Kx.getLabel=Hx.getLabel,ox.registerClass(qx);var Jx=function(){function t(t,e,n){this._prepareParams(t,e,n)}return t.prototype._prepareParams=function(t,e,n){n[1]0&&s>0&&!l&&(a=0),a<0&&s<0&&!u&&(s=0));var c=this._determinedMin,p=this._determinedMax;return null!=c&&(a=c,l=!0),null!=p&&(s=p,u=!0),{min:a,max:s,minFixed:l,maxFixed:u,isBlank:h}},t.prototype.modifyDataMinMax=function(t,e){this[t_[t]]=e},t.prototype.setDeterminedMinMax=function(t,e){var n=Qx[t];this[n]=e},t.prototype.freeze=function(){this.frozen=!0},t}(),Qx={min:"_determinedMin",max:"_determinedMax"},t_={min:"_dataMin",max:"_dataMax"};function e_(t,e,n){var i=t.rawExtentInfo;return i||(i=new Jx(t,e,n),t.rawExtentInfo=i,i)}function n_(t,e){return null==e?null:nt(e)?NaN:t.parse(e)}function i_(t,e){var n=t.type,i=e_(t,e,t.getExtent()).calculate();t.setBlank(i.isBlank);var r=i.min,o=i.max,a=e.ecModel;if(a&&"time"===n){var s=Cx("bar",a),l=!1;if(E(s,(function(t){l=l||t.getBaseAxis()===e.axis})),l){var u=Dx(s),h=function(t,e,n,i){var r=n.axis.getExtent(),o=r[1]-r[0],a=function(t,e,n){if(t&&e){var i=t[Tx(e)];return null!=i&&null!=n?i[Ix(n)]:i}}(i,n.axis);if(void 0===a)return{min:t,max:e};var s=1/0;E(a,(function(t){s=Math.min(t.offset,s)}));var l=-1/0;E(a,(function(t){l=Math.max(t.offset+t.width,l)})),s=Math.abs(s),l=Math.abs(l);var u=s+l,h=e-t,c=h/(1-(s+l)/o)-h;return{min:t-=c*(s/u),max:e+=c*(l/u)}}(r,o,e,u);r=h.min,o=h.max}}return{extent:[r,o],fixMin:i.minFixed,fixMax:i.maxFixed}}function r_(t,e){var n=e,i=i_(t,n),r=i.extent,o=n.get("splitNumber");t instanceof qx&&(t.base=n.get("logBase"));var a=t.type,s=n.get("interval"),l="interval"===a||"time"===a;t.setExtent(r[0],r[1]),t.calcNiceExtent({splitNumber:o,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&t.setInterval&&t.setInterval(s)}function o_(t,e){if(e=e||t.get("type"))switch(e){case"category":return new vx({ordinalMeta:t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),extent:[1/0,-1/0]});case"time":return new Rx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new(ox.getClass(e)||xx)}}function a_(t){var e,n,i=t.getLabelModel().get("formatter"),r="category"===t.type?t.scale.getExtent()[0]:null;return"time"===t.scale.type?(n=i,function(e,i){return t.scale.getFormattedLabel(e,i,n)}):X(i)?function(e){return function(n){var i=t.scale.getLabel(n);return e.replace("{value}",null!=i?i:"")}}(i):U(i)?(e=i,function(n,i){return null!=r&&(i=n.value-r),e(s_(t,n),i,null!=n.level?{level:n.level}:null)}):function(e){return t.scale.getLabel(e)}}function s_(t,e){return"category"===t.type?t.scale.getLabel(e):e.value}function l_(t,e){var n=e*Math.PI/180,i=t.width,r=t.height,o=i*Math.abs(Math.cos(n))+Math.abs(r*Math.sin(n)),a=i*Math.abs(Math.sin(n))+Math.abs(r*Math.cos(n));return new sr(t.x,t.y,o,a)}function u_(t){var e=t.get("interval");return null==e?"auto":e}function h_(t){return"category"===t.type&&0===u_(t.getLabelModel())}function c_(t,e){var n={};return E(t.mapDimensionsAll(e),(function(e){n[ix(t,e)]=!0})),G(n)}var p_=function(){function t(){}return t.prototype.getNeedCrossZero=function(){return!this.option.scale},t.prototype.getCoordSysModel=function(){},t}();var d_={isDimensionStacked:nx,enableDataStack:ex,getStackedDimension:ix};var f_=Object.freeze({__proto__:null,createList:function(t){return rx(null,t)},getLayoutRect:xp,dataStack:d_,createScale:function(t,e){var n=e;e instanceof dc||(n=new dc(e));var i=o_(n);return i.setExtent(t[0],t[1]),r_(i,n),i},mixinAxisModelCommonMethods:function(t){R(t,p_)},getECData:Hs,createTextStyle:function(t,e){return Uh(t,null,null,"normal"!==(e=e||{}).state)},createDimensions:function(t,e){return Km(t,e).dimensions},createSymbol:Ly,enableHoverEmphasis:Ol});function g_(t,e){return Math.abs(t-e)<1e-8}function y_(t,e,n){var i=0,r=t[0];if(!r)return!1;for(var o=1;on&&(t=r,n=a)}if(t)return function(t){for(var e=0,n=0,i=0,r=t.length,o=t[r-1][0],a=t[r-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function C_(t,e){return z(B((t=function(t){if(!t.UTF8Encoding)return t;var e=t,n=e.UTF8Scale;return null==n&&(n=1024),E(e.features,(function(t){var e=t.geometry,i=e.encodeOffsets,r=e.coordinates;if(i)switch(e.type){case"LineString":e.coordinates=T_(r,i,n);break;case"Polygon":case"MultiLineString":I_(r,i,n);break;case"MultiPolygon":E(r,(function(t,e){return I_(t,i[e],n)}))}})),e.UTF8Encoding=!1,e}(t)).features,(function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0})),(function(t){var n=t.properties,i=t.geometry,r=[];switch(i.type){case"Polygon":var o=i.coordinates;r.push(new b_(o[0],o.slice(1)));break;case"MultiPolygon":E(i.coordinates,(function(t){t[0]&&r.push(new b_(t[0],t.slice(1)))}));break;case"LineString":r.push(new w_([i.coordinates]));break;case"MultiLineString":r.push(new w_(i.coordinates))}var a=new S_(n[e||"name"],r,n.cp);return a.properties=n,a}))}var D_=Object.freeze({__proto__:null,linearMap:Nr,round:zr,asc:Vr,getPrecision:Br,getPrecisionSafe:Fr,getPixelPrecision:Gr,getPercentWithPrecision:Wr,MAX_SAFE_INTEGER:Yr,remRadian:Ur,isRadianAroundZero:Xr,parseDate:jr,quantity:qr,quantityExponent:Kr,nice:$r,quantile:Jr,reformIntervals:Qr,isNumeric:eo,numericToNumber:to}),A_=Object.freeze({__proto__:null,parse:jr,format:Vc}),k_=Object.freeze({__proto__:null,extendShape:fh,extendPath:yh,makePath:xh,makeImage:_h,mergePath:wh,resizePath:Sh,createIcon:Ph,updateProps:rh,initProps:oh,getTransform:Ih,clipPointsByRect:kh,clipRectByRect:Lh,registerShape:vh,getShapeClass:mh,Group:Cr,Image:_s,Text:ks,Circle:hu,Ellipse:pu,Sector:Cu,Ring:Au,Polygon:Pu,Polyline:Ru,Rect:Cs,Line:zu,BezierCurve:Gu,Arc:Hu,IncrementalDisplayable:th,CompoundPath:Yu,LinearGradient:Xu,RadialGradient:Zu,BoundingRect:sr}),L_=Object.freeze({__proto__:null,addCommas:ep,toCamelCase:np,normalizeCssArray:ip,encodeHTML:ap,formatTpl:hp,getTooltipMarker:cp,formatTime:function(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=jr(e),r=n?"getUTC":"get",o=i[r+"FullYear"](),a=i[r+"Month"]()+1,s=i[r+"Date"](),l=i[r+"Hours"](),u=i[r+"Minutes"](),h=i[r+"Seconds"](),c=i[r+"Milliseconds"]();return t=t.replace("MM",Nc(a,2)).replace("M",a).replace("yyyy",o).replace("yy",Nc(o%100+"",2)).replace("dd",Nc(s,2)).replace("d",s).replace("hh",Nc(l,2)).replace("h",l).replace("mm",Nc(u,2)).replace("m",u).replace("ss",Nc(h,2)).replace("s",h).replace("SSS",Nc(c,3))},capitalFirst:function(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t},truncateText:$o,getTextRect:function(t,e,n,i,r,o,a,s){return new ks({style:{text:t,font:e,align:n,verticalAlign:i,padding:r,rich:o,overflow:a?"truncate":null,lineHeight:s}}).getBoundingRect()}}),P_=Object.freeze({__proto__:null,map:z,each:E,indexOf:P,inherits:O,reduce:V,filter:B,bind:W,curry:H,isArray:Y,isString:X,isObject:q,isFunction:U,extend:A,defaults:k,clone:T,merge:C}),O_=So();function R_(t){return"category"===t.type?function(t){var e=t.getLabelModel(),n=E_(t,e);return!e.get("show")||t.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(t):function(t){var e=t.scale.getTicks(),n=a_(t);return{labels:z(e,(function(e,i){return{level:e.level,formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tickValue:e.value}}))}}(t)}function N_(t,e){return"category"===t.type?function(t,e){var n,i,r=z_(t,"ticks"),o=u_(e),a=V_(r,o);if(a)return a;e.get("show")&&!t.scale.isBlank()||(n=[]);if(U(o))n=G_(t,o,!0);else if("auto"===o){var s=E_(t,t.getLabelModel());i=s.labelCategoryInterval,n=z(s.labels,(function(t){return t.tickValue}))}else n=F_(t,i=o,!0);return B_(r,o,{ticks:n,tickCategoryInterval:i})}(t,e):{ticks:z(t.scale.getTicks(),(function(t){return t.value}))}}function E_(t,e){var n,i,r=z_(t,"labels"),o=u_(e),a=V_(r,o);return a||(U(o)?n=G_(t,o):(i="auto"===o?function(t){var e=O_(t).autoInterval;return null!=e?e:O_(t).autoInterval=t.calculateCategoryInterval()}(t):o,n=F_(t,i)),B_(r,o,{labels:n,labelCategoryInterval:i}))}function z_(t,e){return O_(t)[e]||(O_(t)[e]=[])}function V_(t,e){for(var n=0;n1&&h/l>2&&(u=Math.round(Math.ceil(u/l)*l));var c=h_(t),p=a.get("showMinLabel")||c,d=a.get("showMaxLabel")||c;p&&u!==o[0]&&g(o[0]);for(var f=u;f<=o[1];f+=l)g(f);function g(t){var e={value:t};s.push(n?t:{formattedLabel:i(e),rawLabel:r.getLabel(e),tickValue:t})}return d&&f-l!==o[1]&&g(o[1]),s}function G_(t,e,n){var i=t.scale,r=a_(t),o=[];return E(i.getTicks(),(function(t){var a=i.getLabel(t),s=t.value;e(t.value,a)&&o.push(n?s:{formattedLabel:r(t),rawLabel:a,tickValue:s})})),o}var W_=[0,1],H_=function(){function t(t,e,n){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=e,this._extent=n||[0,0]}return t.prototype.contain=function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},t.prototype.containData=function(t){return this.scale.contain(t)},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.getPixelPrecision=function(t){return Gr(t||this.scale.getExtent(),this._extent)},t.prototype.setExtent=function(t,e){var n=this._extent;n[0]=t,n[1]=e},t.prototype.dataToCoord=function(t,e){var n=this._extent,i=this.scale;return t=i.normalize(t),this.onBand&&"ordinal"===i.type&&Y_(n=n.slice(),i.count()),Nr(t,W_,n,e)},t.prototype.coordToData=function(t,e){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&Y_(n=n.slice(),i.count());var r=Nr(t,n,W_,e);return this.scale.scale(r)},t.prototype.pointToData=function(t,e){},t.prototype.getTicksCoords=function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=z(N_(this,e).ticks,(function(t){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(t):t),tickValue:t}}),this);return function(t,e,n,i){var r=e.length;if(!t.onBand||n||!r)return;var o,a,s=t.getExtent();if(1===r)e[0].coord=s[0],o=e[1]={coord:s[0]};else{var l=e[r-1].tickValue-e[0].tickValue,u=(e[r-1].coord-e[0].coord)/l;E(e,(function(t){t.coord-=u/2})),a=1+t.scale.getExtent()[1]-e[r-1].tickValue,o={coord:e[r-1].coord+u*a},e.push(o)}var h=s[0]>s[1];c(e[0].coord,s[0])&&(i?e[0].coord=s[0]:e.shift());i&&c(s[0],e[0].coord)&&e.unshift({coord:s[0]});c(s[1],o.coord)&&(i?o.coord=s[1]:e.pop());i&&c(o.coord,s[1])&&e.push({coord:s[1]});function c(t,e){return t=zr(t),e=zr(e),h?t>e:t0&&t<100||(t=5),z(this.scale.getMinorTicks(t),(function(t){return z(t,(function(t){return{coord:this.dataToCoord(t),tickValue:t}}),this)}),this)},t.prototype.getViewLabels=function(){return R_(this).labels},t.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},t.prototype.getTickModel=function(){return this.model.getModel("axisTick")},t.prototype.getBandWidth=function(){var t=this._extent,e=this.scale.getExtent(),n=e[1]-e[0]+(this.onBand?1:0);0===n&&(n=1);var i=Math.abs(t[1]-t[0]);return Math.abs(i)/n},t.prototype.calculateCategoryInterval=function(){return function(t){var e=function(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}(t),n=a_(t),i=(e.axisRotate-e.labelRotate)/180*Math.PI,r=t.scale,o=r.getExtent(),a=r.count();if(o[1]-o[0]<1)return 0;var s=1;a>40&&(s=Math.max(1,Math.floor(a/40)));for(var l=o[0],u=t.dataToCoord(l+1)-t.dataToCoord(l),h=Math.abs(u*Math.cos(i)),c=Math.abs(u*Math.sin(i)),p=0,d=0;l<=o[1];l+=s){var f,g,y=cr(n({value:l}),e.font,"center","top");f=1.3*y.width,g=1.3*y.height,p=Math.max(p,f,7),d=Math.max(d,g,7)}var v=p/h,m=d/c;isNaN(v)&&(v=1/0),isNaN(m)&&(m=1/0);var x=Math.max(0,Math.floor(Math.min(v,m))),_=O_(t.model),b=t.getExtent(),w=_.lastAutoInterval,S=_.lastTickCount;return null!=w&&null!=S&&Math.abs(w-x)<=1&&Math.abs(S-a)<=1&&w>x&&_.axisExtent0===b[0]&&_.axisExtent1===b[1]?x=w:(_.lastTickCount=a,_.lastAutoInterval=x,_.axisExtent0=b[0],_.axisExtent1=b[1]),x}(this)},t}();function Y_(t,e){var n=(t[1]-t[0])/e/2;t[0]+=n,t[1]-=n}var U_=2*Math.PI,X_=qa.CMD,Z_=["top","right","bottom","left"];function j_(t,e,n,i,r){var o=n.width,a=n.height;switch(t){case"top":i.set(n.x+o/2,n.y-e),r.set(0,-1);break;case"bottom":i.set(n.x+o/2,n.y+a+e),r.set(0,1);break;case"left":i.set(n.x-e,n.y+a/2),r.set(-1,0);break;case"right":i.set(n.x+o+e,n.y+a/2),r.set(1,0)}}function q_(t,e,n,i,r,o,a,s,l){a-=t,s-=e;var u=Math.sqrt(a*a+s*s),h=(a/=u)*n+t,c=(s/=u)*n+e;if(Math.abs(i-r)%U_<1e-4)return l[0]=h,l[1]=c,u-n;if(o){var p=i;i=ts(r),r=ts(p)}else i=ts(i),r=ts(r);i>r&&(r+=U_);var d=Math.atan2(s,a);if(d<0&&(d+=U_),d>=i&&d<=r||d+U_>=i&&d+U_<=r)return l[0]=h,l[1]=c,u-n;var f=n*Math.cos(i)+t,g=n*Math.sin(i)+e,y=n*Math.cos(r)+t,v=n*Math.sin(r)+e,m=(f-a)*(f-a)+(g-s)*(g-s),x=(y-a)*(y-a)+(v-s)*(v-s);return m0){e=e/180*Math.PI,eb.fromArray(t[0]),nb.fromArray(t[1]),ib.fromArray(t[2]),Ji.sub(rb,eb,nb),Ji.sub(ob,ib,nb);var n=rb.len(),i=ob.len();if(!(n<.001||i<.001)){rb.scale(1/n),ob.scale(1/i);var r=rb.dot(ob);if(Math.cos(e)1&&Ji.copy(lb,ib),lb.toArray(t[1])}}}}function hb(t,e,n){if(n<=180&&n>0){n=n/180*Math.PI,eb.fromArray(t[0]),nb.fromArray(t[1]),ib.fromArray(t[2]),Ji.sub(rb,nb,eb),Ji.sub(ob,ib,nb);var i=rb.len(),r=ob.len();if(!(i<.001||r<.001))if(rb.scale(1/i),ob.scale(1/r),rb.dot(e)=a)Ji.copy(lb,ib);else{lb.scaleAndAdd(ob,o/Math.tan(Math.PI/2-s));var l=ib.x!==nb.x?(lb.x-nb.x)/(ib.x-nb.x):(lb.y-nb.y)/(ib.y-nb.y);if(isNaN(l))return;l<0?Ji.copy(lb,nb):l>1&&Ji.copy(lb,ib)}lb.toArray(t[1])}}}function cb(t,e,n,i){var r="normal"===n,o=r?t:t.ensureState(n);o.ignore=e;var a=i.get("smooth");a&&!0===a&&(a=.3),o.shape=o.shape||{},a>0&&(o.shape.smooth=a);var s=i.getModel("lineStyle").getLineStyle();r?t.useStyle(s):o.style=s}function pb(t,e){var n=e.smooth,i=e.points;if(i)if(t.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var r=Et(i[0],i[1]),o=Et(i[1],i[2]);if(!r||!o)return t.lineTo(i[1][0],i[1][1]),void t.lineTo(i[2][0],i[2][1]);var a=Math.min(r,o)*n,s=Bt([],i[1],i[0],a/r),l=Bt([],i[1],i[2],a/o),u=Bt([],s,l,.5);t.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),t.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var h=1;h0&&o&&_(-h/a,0,a);var f,g,y=t[0],v=t[a-1];return m(),f<0&&b(-f,.8),g<0&&b(g,.8),m(),x(f,g,1),x(g,f,-1),m(),f<0&&w(-f),g<0&&w(g),u}function m(){f=y.rect[e]-i,g=r-v.rect[e]-v.rect[n]}function x(t,e,n){if(t<0){var i=Math.min(e,-t);if(i>0){_(i*n,0,a);var r=i+t;r<0&&b(-r*n,1)}else b(-t*n,1)}}function _(n,i,r){0!==n&&(u=!0);for(var o=i;o0)for(l=0;l0;l--){_(-(o[l-1]*c),l,a)}}}function w(t){var e=t<0?-1:1;t=Math.abs(t);for(var n=Math.ceil(t/(a-1)),i=0;i0?_(n,0,i+1):_(-n,a-i-1,a),(t-=n)<=0)return}}function vb(t,e,n,i){return yb(t,"y","height",e,n,i)}function mb(t){var e=[];t.sort((function(t,e){return e.priority-t.priority}));var n=new sr(0,0,0,0);function i(t){if(!t.ignore){var e=t.ensureState("emphasis");null==e.ignore&&(e.ignore=!1)}t.ignore=!0}for(var r=0;r=0&&n.attr(d.oldLayoutSelect),P(u,"emphasis")>=0&&n.attr(d.oldLayoutEmphasis)),rh(n,s,e,a)}else if(n.attr(s),!Jh(n).valueAnimation){var h=rt(n.style.opacity,1);n.style.opacity=0,oh(n,{style:{opacity:h}},e,a)}if(d.oldLayout=s,n.states.select){var c=d.oldLayoutSelect={};Ib(c,s,Tb),Ib(c,n.states.select,Tb)}if(n.states.emphasis){var p=d.oldLayoutEmphasis={};Ib(p,s,Tb),Ib(p,n.states.emphasis,Tb)}tc(n,a,l,e,e)}if(i&&!i.ignore&&!i.invisible){r=(d=Mb(i)).oldLayout;var d,f={points:i.shape.points};r?(i.attr({shape:r}),rh(i,{shape:f},e)):(i.setShape(f),i.style.strokePercent=0,oh(i,{style:{strokePercent:1}},e)),d.oldLayout=f}},t}(),Db=So();var Ab=Math.sin,kb=Math.cos,Lb=Math.PI,Pb=2*Math.PI,Ob=180/Lb,Rb=function(){function t(){}return t.prototype.reset=function(t){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,t||4)},t.prototype.moveTo=function(t,e){this._add("M",t,e)},t.prototype.lineTo=function(t,e){this._add("L",t,e)},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){this._add("C",t,e,n,i,r,o)},t.prototype.quadraticCurveTo=function(t,e,n,i){this._add("Q",t,e,n,i)},t.prototype.arc=function(t,e,n,i,r,o){this.ellipse(t,e,n,n,0,i,r,o)},t.prototype.ellipse=function(t,e,n,i,r,o,a,s){var l=a-o,u=!s,h=Math.abs(l),c=En(h-Pb)||(u?l>=Pb:-l>=Pb),p=l>0?l%Pb:l%Pb+Pb,d=!1;d=!!c||!En(h)&&p>=Lb==!!u;var f=t+n*kb(o),g=e+i*Ab(o);this._start&&this._add("M",f,g);var y=Math.round(r*Ob);if(c){var v=1/this._p,m=(u?1:-1)*(Pb-v);this._add("A",n,i,y,1,+u,t+n*kb(o+m),e+i*Ab(o+m)),v>.01&&this._add("A",n,i,y,0,+u,f,g)}else{var x=t+n*kb(a),_=e+i*Ab(a);this._add("A",n,i,y,+d,+u,x,_)}},t.prototype.rect=function(t,e,n,i){this._add("M",t,e),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},t.prototype.closePath=function(){this._d.length>0&&this._add("Z")},t.prototype._add=function(t,e,n,i,r,o,a,s,l){for(var u=[],h=this._p,c=1;c"}(r,e.attrs)+(e.text||"")+(i?""+n+z(i,(function(e){return t(e)})).join(n)+n:"")+("")}(t)}function Ub(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssClassIdx:0,cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function Xb(t,e,n,i){return Hb("svg","root",{width:t,height:e,xmlns:Fb,"xmlns:xlink":Gb,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+t+" "+e},n)}var Zb={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},jb="transform-origin";function qb(t,e,n){var i=A({},t.shape);A(i,e),t.buildPath(n,i);var r=new Rb;return r.reset(Yn(t)),n.rebuildPath(r,1),r.generateStr(),r.getStr()}function Kb(t,e){var n=e.originX,i=e.originY;(n||i)&&(t[jb]=n+"px "+i+"px")}var $b={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function Jb(t,e){var n=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[n]=t,n}function Qb(t){return X(t)?Zb[t]?"cubic-bezier("+Zb[t]+")":rn(t)?t:"":""}function tw(t,e,n,i){var r=t.animators,o=r.length,a=[];if(t instanceof Yu){if(y=function(t,e,n){var i,r,o=t.shape.paths,a={};if(E(o,(function(t){var e=Ub(n.zrId);e.animation=!0,tw(t,{},e,!0);var o=e.cssAnims,s=e.cssNodes,l=G(o),u=l.length;if(u){var h=o[r=l[u-1]];for(var c in h){var p=h[c];a[c]=a[c]||{d:""},a[c].d+=p.d||""}for(var d in s){var f=s[d].animation;f.indexOf(r)>=0&&(i=f)}}})),i){e.d=!1;var s=Jb(a,n);return i.replace(r,s)}}(t,e,n))a.push(y);else if(!o)return}else if(!o)return;for(var s={},l=0;l0})).length)return Jb(h,n)+" "+r[0]+" both"}for(var g in s){var y;(y=f(s[g]))&&a.push(y)}if(a.length){var v=n.zrId+"-cls-"+n.cssClassIdx++;n.cssNodes["."+v]={animation:a.join(",")},e.class=v}}var ew=Math.round;function nw(t){return t&&X(t.src)}function iw(t){return t&&U(t.toDataURL)}function rw(t,e,n,i){Bb((function(r,o){var a="fill"===r||"stroke"===r;a&&function(t){return t&&("linear"===t.type||"radial"===t.type)}(o)?function(t,e,n,i){var r,o=t[n],a={gradientUnits:o.global?"userSpaceOnUse":"objectBoundingBox"};if(Gn(o))r="linearGradient",a.x1=o.x,a.y1=o.y,a.x2=o.x2,a.y2=o.y2;else{if(!Wn(o))return void 0;r="radialGradient",a.cx=rt(o.x,.5),a.cy=rt(o.y,.5),a.r=rt(o.r,.5)}for(var s=o.colorStops,l=[],u=0,h=s.length;ul?Dw(t,null==n[c+1]?null:n[c+1].elm,n,s,c):Aw(t,e,a,l))}(n,i,r):Mw(r)?(Mw(t.text)&&bw(n,""),Dw(n,null,r,0,r.length-1)):Mw(i)?Aw(n,i,0,i.length-1):Mw(t.text)&&bw(n,""):t.text!==e.text&&(Mw(i)&&Aw(n,i,0,i.length-1),bw(n,e.text)))}var Pw=0,Ow=function(){function t(t,e,n){if(this.type="svg",this.refreshHover=Rw("refreshHover"),this.configLayer=Rw("configLayer"),this.storage=e,this._opts=n=A({},n),this.root=t,this._id="zr"+Pw++,this._oldVNode=Xb(n.width,n.height),t&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var r=this._svgDom=this._oldVNode.elm=Wb("svg");kw(null,this._oldVNode),i.appendChild(r),t.appendChild(i)}this.resize(n.width,n.height)}return t.prototype.getType=function(){return this.type},t.prototype.getViewportRoot=function(){return this._viewport},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.getSvgDom=function(){return this._svgDom},t.prototype.refresh=function(){if(this.root){var t=this.renderToVNode({willUpdate:!0});t.attrs.style="position:absolute;left:0;top:0;user-select:none",function(t,e){if(Tw(t,e))Lw(t,e);else{var n=t.elm,i=xw(n);Cw(e),null!==i&&(yw(i,e.elm,_w(n)),Aw(i,[t],0,0))}}(this._oldVNode,t),this._oldVNode=t}},t.prototype.renderOneToVNode=function(t){return dw(t,Ub(this._id))},t.prototype.renderToVNode=function(t){t=t||{};var e=this.storage.getDisplayList(!0),n=this._backgroundColor,i=this._width,r=this._height,o=Ub(this._id);o.animation=t.animation,o.willUpdate=t.willUpdate,o.compress=t.compress;var a=[];if(n&&"none"!==n){var s=Rn(n),l=s.color,u=s.opacity;this._bgVNode=Hb("rect","bg",{width:i,height:r,x:"0",y:"0",id:"0",fill:l,"fill-opacity":u}),a.push(this._bgVNode)}else this._bgVNode=null;var h=t.compress?null:this._mainVNode=Hb("g","main",{},[]);this._paintList(e,o,h?h.children:a),h&&a.push(h);var c=z(G(o.defs),(function(t){return o.defs[t]}));if(c.length&&a.push(Hb("defs","defs",{},c)),t.animation){var p=function(t,e,n){var i=(n=n||{}).newline?"\n":"",r=" {"+i,o=i+"}",a=z(G(t),(function(e){return e+r+z(G(t[e]),(function(n){return n+":"+t[e][n]+";"})).join(i)+o})).join(i),s=z(G(e),(function(t){return"@keyframes "+t+r+z(G(e[t]),(function(n){return n+r+z(G(e[t][n]),(function(i){var r=e[t][n][i];return"d"===i&&(r='path("'+r+'")'),i+":"+r+";"})).join(i)+o})).join(i)+o})).join(i);return a||s?[""].join(i):""}(o.cssNodes,o.cssAnims,{newline:!0});if(p){var d=Hb("style","stl",{},[],p);a.push(d)}}return Xb(i,r,a,t.useViewBox)},t.prototype.renderToString=function(t){return t=t||{},Yb(this.renderToVNode({animation:rt(t.cssAnimation,!0),willUpdate:!1,compress:!0,useViewBox:rt(t.useViewBox,!0)}),{newline:!0})},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t;var e=this._bgVNode;if(e&&e.elm){var n=Rn(t),i=n.color,r=n.opacity;e.elm.setAttribute("fill",i),r<1&&e.elm.setAttribute("fill-opacity",r)}},t.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},t.prototype._paintList=function(t,e,n){for(var i,r,o=t.length,a=[],s=0,l=0,u=0;u=0&&(!c||!r||c[f]!==r[f]);f--);for(var g=d-1;g>f;g--)i=a[--s-1];for(var y=f+1;y=a)}}for(var h=this.__startIndex;h15)break}n.prevElClipPaths&&u.restore()};if(p)if(0===p.length)s=l.__endIndex;else for(var _=d.dpr,b=0;b0&&t>i[0]){for(s=0;st);s++);a=n[i[s]]}if(i.splice(s+1,0,t),n[t]=e,!e.virtual)if(a){var l=a.dom;l.nextSibling?o.insertBefore(e.dom,l.nextSibling):o.appendChild(e.dom)}else o.firstChild?o.insertBefore(e.dom,o.firstChild):o.appendChild(e.dom);e.__painter=this}},t.prototype.eachLayer=function(t,e){for(var n=this._zlevelList,i=0;i0?Bw:0),this._needsManuallyCompositing),u.__builtin__||I("ZLevel "+l+" has been used by unkown layer "+u.id),u!==o&&(u.__used=!0,u.__startIndex!==r&&(u.__dirty=!0),u.__startIndex=r,u.incremental?u.__drawIndex=-1:u.__drawIndex=r,e(r),o=u),1&s.__dirty&&!s.__inHover&&(u.__dirty=!0,u.incremental&&u.__drawIndex<0&&(u.__drawIndex=r))}e(r),this.eachBuiltinLayer((function(t,e){!t.__used&&t.getElementCount()>0&&(t.__dirty=!0,t.__startIndex=t.__endIndex=t.__drawIndex=0),t.__dirty&&t.__drawIndex<0&&(t.__drawIndex=t.__startIndex)}))},t.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},t.prototype._clearLayer=function(t){t.clear()},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t,E(this._layers,(function(t){t.setUnpainted()}))},t.prototype.configLayer=function(t,e){if(e){var n=this._layerConfig;n[t]?C(n[t],e,!0):n[t]=e;for(var i=0;i-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),e},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},e}(sg);function Ww(t,e){var n=t.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var r=af(t,e,n[0]);return null!=r?r+"":null}if(i){for(var o=[],a=0;a=0&&i.push(e[o])}return i.join(" ")}var Yw=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o.updateData(e,n,i,r),o}return n(e,t),e.prototype._createSymbol=function(t,e,n,i,r){this.removeAll();var o=Ly(t,-1,-1,2,2,null,r);o.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),o.drift=Uw,this._symbolType=t,this.add(o)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){_l(this.childAt(0))},e.prototype.downplay=function(){bl(this.childAt(0))},e.prototype.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},e.prototype.setDraggable=function(t,e){var n=this.childAt(0);n.draggable=t,n.cursor=!e&&t?"move":n.cursor},e.prototype.updateData=function(t,n,i,r){this.silent=!1;var o=t.getItemVisual(n,"symbol")||"circle",a=t.hostModel,s=e.getSymbolSize(t,n),l=o!==this._symbolType,u=r&&r.disableAnimation;if(l){var h=t.getItemVisual(n,"symbolKeepAspect");this._createSymbol(o,t,n,s,h)}else{(p=this.childAt(0)).silent=!1;var c={scaleX:s[0]/2,scaleY:s[1]/2};u?p.attr(c):rh(p,c,a,n),hh(p)}if(this._updateCommon(t,n,s,i,r),l){var p=this.childAt(0);if(!u){c={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:p.style.opacity}};p.scaleX=p.scaleY=0,p.style.opacity=0,oh(p,c,a,n)}}u&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,e,n,i,r){var o,a,s,l,u,h,c,p,d,f=this.childAt(0),g=t.hostModel;if(i&&(o=i.emphasisItemStyle,a=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,u=i.blurScope,c=i.labelStatesModels,p=i.hoverScale,d=i.cursorStyle,h=i.emphasisDisabled),!i||t.hasItemOption){var y=i&&i.itemModel?i.itemModel:t.getItemModel(e),v=y.getModel("emphasis");o=v.getModel("itemStyle").getItemStyle(),s=y.getModel(["select","itemStyle"]).getItemStyle(),a=y.getModel(["blur","itemStyle"]).getItemStyle(),l=v.get("focus"),u=v.get("blurScope"),h=v.get("disabled"),c=Yh(y),p=v.getShallow("scale"),d=y.getShallow("cursor")}var m=t.getItemVisual(e,"symbolRotate");f.attr("rotation",(m||0)*Math.PI/180||0);var x=Oy(t.getItemVisual(e,"symbolOffset"),n);x&&(f.x=x[0],f.y=x[1]),d&&f.attr("cursor",d);var _=t.getItemVisual(e,"style"),b=_.fill;if(f instanceof _s){var w=f.style;f.useStyle(A({image:w.image,x:w.x,y:w.y,width:w.width,height:w.height},_))}else f.__isEmptyBrush?f.useStyle(A({},_)):f.useStyle(_),f.style.decal=null,f.setColor(b,r&&r.symbolInnerColor),f.style.strokeNoScale=!0;var S=t.getItemVisual(e,"liftZ"),M=this._z2;null!=S?null==M&&(this._z2=f.z2,f.z2+=S):null!=M&&(f.z2=M,this._z2=null);var I=r&&r.useNameLabel;Hh(f,c,{labelFetcher:g,labelDataIndex:e,defaultText:function(e){return I?t.getName(e):Ww(t,e)},inheritColor:b,defaultOpacity:_.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var T=f.ensureState("emphasis");if(T.style=o,f.ensureState("select").style=s,f.ensureState("blur").style=a,p){var C=Math.max(j(p)?p:1.1,3/this._sizeY);T.scaleX=this._sizeX*C,T.scaleY=this._sizeY*C}this.setSymbolScale(1),Rl(this,l,u,h)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,e,n){var i=this.childAt(0),r=Hs(this).dataIndex,o=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var a=i.getTextContent();a&&sh(a,{style:{opacity:0}},e,{dataIndex:r,removeOpt:o,cb:function(){i.removeTextContent()}})}else i.removeTextContent();sh(i,{style:{opacity:0},scaleX:0,scaleY:0},e,{dataIndex:r,cb:t,removeOpt:o})},e.getSymbolSize=function(t,e){return Py(t.getItemVisual(e,"symbolSize"))},e}(Cr);function Uw(t,e){this.parent.drift(t,e)}function Xw(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function Zw(t){return null==t||q(t)||(t={isIgnore:t}),t||{}}function jw(t){var e=t.hostModel,n=e.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:Yh(e),cursorStyle:e.get("cursor")}}var qw=function(){function t(t){this.group=new Cr,this._SymbolCtor=t||Yw}return t.prototype.updateData=function(t,e){this._progressiveEls=null,e=Zw(e);var n=this.group,i=t.hostModel,r=this._data,o=this._SymbolCtor,a=e.disableAnimation,s=jw(t),l={disableAnimation:a},u=e.getSymbolPoint||function(e){return t.getItemLayout(e)};r||n.removeAll(),t.diff(r).add((function(i){var r=u(i);if(Xw(t,r,i,e)){var a=new o(t,i,s,l);a.setPosition(r),t.setItemGraphicEl(i,a),n.add(a)}})).update((function(h,c){var p=r.getItemGraphicEl(c),d=u(h);if(Xw(t,d,h,e)){var f=t.getItemVisual(h,"symbol")||"circle",g=p&&p.getSymbolType&&p.getSymbolType();if(!p||g&&g!==f)n.remove(p),(p=new o(t,h,s,l)).setPosition(d);else{p.updateData(t,h,s,l);var y={x:d[0],y:d[1]};a?p.attr(y):rh(p,y,i)}n.add(p),t.setItemGraphicEl(h,p)}else n.remove(p)})).remove((function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut((function(){n.remove(e)}),i)})).execute(),this._getSymbolPoint=u,this._data=t},t.prototype.updateLayout=function(){var t=this,e=this._data;e&&e.eachItemGraphicEl((function(e,n){var i=t._getSymbolPoint(n);e.setPosition(i),e.markRedraw()}))},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=jw(t),this._data=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e,n){function i(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=Zw(n);for(var r=t.start;r0?n=i[0]:i[1]<0&&(n=i[1]);return n}(r,n),a=i.dim,s=r.dim,l=e.mapDimension(s),u=e.mapDimension(a),h="x"===s||"radius"===s?1:0,c=z(t.dimensions,(function(t){return e.mapDimension(t)})),p=!1,d=e.getCalculationInfo("stackResultDimension");return nx(e,c[0])&&(p=!0,c[0]=d),nx(e,c[1])&&(p=!0,c[1]=d),{dataDimsForPoint:c,valueStart:o,valueAxisDim:s,baseAxisDim:a,stacked:!!p,valueDim:l,baseDim:u,baseDataOffset:h,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function $w(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}var Jw=Math.min,Qw=Math.max;function tS(t,e){return isNaN(t)||isNaN(e)}function eS(t,e,n,i,r,o,a,s,l){for(var u,h,c,p,d,f,g=n,y=0;y=r||g<0)break;if(tS(v,m)){if(l){g+=o;continue}break}if(g===n)t[o>0?"moveTo":"lineTo"](v,m),c=v,p=m;else{var x=v-u,_=m-h;if(x*x+_*_<.5){g+=o;continue}if(a>0){for(var b=g+o,w=e[2*b],S=e[2*b+1];w===v&&S===m&&y=i||tS(w,S))d=v,f=m;else{T=w-u,C=S-h;var k=v-u,L=w-v,P=m-h,O=S-m,R=void 0,N=void 0;if("x"===s){var E=T>0?1:-1;d=v-E*(R=Math.abs(k))*a,f=m,D=v+E*(N=Math.abs(L))*a,A=m}else if("y"===s){var z=C>0?1:-1;d=v,f=m-z*(R=Math.abs(P))*a,D=v,A=m+z*(N=Math.abs(O))*a}else R=Math.sqrt(k*k+P*P),d=v-T*a*(1-(I=(N=Math.sqrt(L*L+O*O))/(N+R))),f=m-C*a*(1-I),A=m+C*a*I,D=Jw(D=v+T*a*I,Qw(w,v)),A=Jw(A,Qw(S,m)),D=Qw(D,Jw(w,v)),f=m-(C=(A=Qw(A,Jw(S,m)))-m)*R/N,d=Jw(d=v-(T=D-v)*R/N,Qw(u,v)),f=Jw(f,Qw(h,m)),D=v+(T=v-(d=Qw(d,Jw(u,v))))*N/R,A=m+(C=m-(f=Qw(f,Jw(h,m))))*N/R}t.bezierCurveTo(c,p,d,f,v,m),c=D,p=A}else t.lineTo(v,m)}u=v,h=m,g+=o}return y}var nS=function(){this.smooth=0,this.smoothConstraint=!0},iS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polyline",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new nS},e.prototype.buildPath=function(t,e){var n=e.points,i=0,r=n.length/2;if(e.connectNulls){for(;r>0&&tS(n[2*r-2],n[2*r-1]);r--);for(;i=0){var y=a?(h-i)*g+i:(u-n)*g+n;return a?[t,y]:[y,t]}n=u,i=h;break;case o.C:u=r[l++],h=r[l++],c=r[l++],p=r[l++],d=r[l++],f=r[l++];var v=a?Ue(n,u,c,d,t,s):Ue(i,h,p,f,t,s);if(v>0)for(var m=0;m=0){y=a?He(i,h,p,f,x):He(n,u,c,d,x);return a?[t,y]:[y,t]}}n=d,i=f}}},e}(gs),rS=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(nS),oS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polygon",n}return n(e,t),e.prototype.getDefaultShape=function(){return new rS},e.prototype.buildPath=function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length/2,a=e.smoothMonotone;if(e.connectNulls){for(;o>0&&tS(n[2*o-2],n[2*o-1]);o--);for(;r=0;a--){var s=t.getDimensionInfo(i[a].dimension);if("x"===(r=s&&s.coordDim)||"y"===r){o=i[a];break}}if(o){var l=e.getAxis(r),u=z(o.stops,(function(t){return{coord:l.toGlobalCoord(l.dataToCoord(t.value)),color:t.color}})),h=u.length,c=o.outerColors.slice();h&&u[0].coord>u[h-1].coord&&(u.reverse(),c.reverse());var p=function(t,e){var n,i,r=[],o=t.length;function a(t,e,n){var i=t.coord;return{coord:n,color:Tn((n-i)/(e.coord-i),[t.color,e.color])}}for(var s=0;se){i?r.push(a(i,l,e)):n&&r.push(a(n,l,0),a(n,l,e));break}n&&(r.push(a(n,l,0)),n=null),r.push(l),i=l}}return r}(u,"x"===r?n.getWidth():n.getHeight()),d=p.length;if(!d&&h)return u[0].coord<0?c[1]?c[1]:u[h-1].color:c[0]?c[0]:u[0].color;var f=p[0].coord-10,g=p[d-1].coord+10,y=g-f;if(y<.001)return"transparent";E(p,(function(t){t.offset=(t.coord-f)/y})),p.push({offset:d?p[d-1].offset:.5,color:c[1]||"transparent"}),p.unshift({offset:d?p[0].offset:.5,color:c[0]||"transparent"});var v=new Xu(0,0,0,0,p,!0);return v[r]=f,v[r+"2"]=g,v}}}function yS(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!function(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;ai)return!1;return!0}(o,e))){var a=e.mapDimension(o.dim),s={};return E(o.getViewLabels(),(function(t){var e=o.scale.getRawOrdinalNumber(t.tickValue);s[e]=1})),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function vS(t,e){return[t[2*e],t[2*e+1]]}function mS(t){if(t.get(["endLabel","show"]))return!0;for(var e=0;e0&&"bolder"===t.get(["emphasis","lineStyle","width"]))&&(d.getState("emphasis").style.lineWidth=+d.style.lineWidth+1);Hs(d).seriesIndex=t.seriesIndex,Rl(d,L,P,O);var R=dS(t.get("smooth")),N=t.get("smoothMonotone");if(d.setShape({smooth:R,smoothMonotone:N,connectNulls:w}),f){var E=a.getCalculationInfo("stackedOnSeries"),z=0;f.useStyle(k(l.getAreaStyle(),{fill:C,opacity:.7,lineJoin:"bevel",decal:a.getVisual("style").decal})),E&&(z=dS(E.get("smooth"))),f.setShape({smooth:R,stackedOnSmooth:z,smoothMonotone:N,connectNulls:w}),Vl(f,t,"areaStyle"),Hs(f).seriesIndex=t.seriesIndex,Rl(f,L,P,O)}var V=function(t){i._changePolyState(t)};a.eachItemGraphicEl((function(t){t&&(t.onHoverStateChange=V)})),this._polyline.onHoverStateChange=V,this._data=a,this._coordSys=r,this._stackedOnPoints=_,this._points=u,this._step=T,this._valueOrigin=m,t.get("triggerLineEvent")&&(this.packEventData(t,d),f&&this.packEventData(t,f))},e.prototype.packEventData=function(t,e){Hs(e).eventData={componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line"}},e.prototype.highlight=function(t,e,n,i){var r=t.getData(),o=wo(r,i);if(this._changePolyState("emphasis"),!(o instanceof Array)&&null!=o&&o>=0){var a=r.getLayout("points"),s=r.getItemGraphicEl(o);if(!s){var l=a[2*o],u=a[2*o+1];if(isNaN(l)||isNaN(u))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,u))return;var h=t.get("zlevel"),c=t.get("z");(s=new Yw(r,o)).x=l,s.y=u,s.setZ(h,c);var p=s.getSymbolPath().getTextContent();p&&(p.zlevel=h,p.z=c,p.z2=this._polyline.z2+1),s.__temp=!0,r.setItemGraphicEl(o,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else xg.prototype.highlight.call(this,t,e,n,i)},e.prototype.downplay=function(t,e,n,i){var r=t.getData(),o=wo(r,i);if(this._changePolyState("normal"),null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else xg.prototype.downplay.call(this,t,e,n,i)},e.prototype._changePolyState=function(t){var e=this._polygon;gl(this._polyline,t),e&&gl(e,t)},e.prototype._newPolyline=function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new iS({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(e),this._polyline=e,e},e.prototype._newPolygon=function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new oS({shape:{points:t,stackedOnPoints:e},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,e,n){var i,r,o=e.getBaseAxis(),a=o.inverse;"cartesian2d"===e.type?(i=o.isHorizontal(),r=!1):"polar"===e.type&&(i="angle"===o.dim,r=!0);var s=t.hostModel,l=s.get("animationDuration");U(l)&&(l=l(null));var u=s.get("animationDelay")||0,h=U(u)?u(null):u;t.eachItemGraphicEl((function(t,o){var s=t;if(s){var c=[t.x,t.y],p=void 0,d=void 0,f=void 0;if(n)if(r){var g=n,y=e.pointToCoord(c);i?(p=g.startAngle,d=g.endAngle,f=-y[1]/180*Math.PI):(p=g.r0,d=g.r,f=y[0])}else{var v=n;i?(p=v.x,d=v.x+v.width,f=t.x):(p=v.y+v.height,d=v.y,f=t.y)}var m=d===p?0:(f-p)/(d-p);a&&(m=1-m);var x=U(u)?u(o):l*m+h,_=s.getSymbolPath(),b=_.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),b&&b.animateFrom({style:{opacity:0}},{duration:300,delay:x}),_.disableLabelAnimation=!0}}))},e.prototype._initOrUpdateEndLabel=function(t,e,n){var i=t.getModel("endLabel");if(mS(t)){var r=t.getData(),o=this._polyline,a=r.getLayout("points");if(!a)return o.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new ks({z2:200})).ignoreClip=!0,o.setTextContent(this._endLabel),o.disableLabelAnimation=!0);var l=function(t){for(var e,n,i=t.length/2;i>0&&(e=t[2*i-2],n=t[2*i-1],isNaN(e)||isNaN(n));i--);return i-1}(a);l>=0&&(Hh(o,Yh(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:l,defaultText:function(t,e,n){return null!=n?Hw(r,n):Ww(r,t)},enableTextSetter:!0},function(t,e){var n=e.getBaseAxis(),i=n.isHorizontal(),r=n.inverse,o=i?r?"right":"left":"center",a=i?"middle":r?"top":"bottom";return{normal:{align:t.get("align")||o,verticalAlign:t.get("verticalAlign")||a}}}(i,e)),o.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,e,n,i,r,o,a){var s=this._endLabel,l=this._polyline;if(s){t<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var u=n.getLayout("points"),h=n.hostModel,c=h.get("connectNulls"),p=o.get("precision"),d=o.get("distance")||0,f=a.getBaseAxis(),g=f.isHorizontal(),y=f.inverse,v=e.shape,m=y?g?v.x:v.y+v.height:g?v.x+v.width:v.y,x=(g?d:0)*(y?-1:1),_=(g?0:-d)*(y?-1:1),b=g?"x":"y",w=function(t,e,n){for(var i,r,o=t.length/2,a="x"===n?0:1,s=0,l=-1,u=0;u=e||i>=e&&r<=e){l=u;break}s=u,i=r}else i=r;return{range:[s,l],t:(e-i)/(r-i)}}(u,m,b),S=w.range,M=S[1]-S[0],I=void 0;if(M>=1){if(M>1&&!c){var T=vS(u,S[0]);s.attr({x:T[0]+x,y:T[1]+_}),r&&(I=h.getRawValue(S[0]))}else{(T=l.getPointOn(m,b))&&s.attr({x:T[0]+x,y:T[1]+_});var C=h.getRawValue(S[0]),D=h.getRawValue(S[1]);r&&(I=Po(n,p,C,D,w.t))}i.lastFrameIndex=S[0]}else{var A=1===t||i.lastFrameIndex>0?S[0]:0;T=vS(u,A);r&&(I=h.getRawValue(A)),s.attr({x:T[0]+x,y:T[1]+_})}r&&Jh(s).setLabelText(I)}},e.prototype._doUpdateAnimation=function(t,e,n,i,r,o,a){var s=this._polyline,l=this._polygon,u=t.hostModel,h=function(t,e,n,i,r,o,a,s){for(var l=function(t,e){var n=[];return e.diff(t).add((function(t){n.push({cmd:"+",idx:t})})).update((function(t,e){n.push({cmd:"=",idx:e,idx1:t})})).remove((function(t){n.push({cmd:"-",idx:t})})).execute(),n}(t,e),u=[],h=[],c=[],p=[],d=[],f=[],g=[],y=Kw(r,e,a),v=t.getLayout("points")||[],m=e.getLayout("points")||[],x=0;x3e3||l&&pS(p,f)>3e3)return s.stopAnimation(),s.setShape({points:d}),void(l&&(l.stopAnimation(),l.setShape({points:d,stackedOnPoints:f})));s.shape.__points=h.current,s.shape.points=c;var g={shape:{points:d}};h.current!==c&&(g.shape.__points=h.next),s.stopAnimation(),rh(s,g,u),l&&(l.setShape({points:c,stackedOnPoints:p}),l.stopAnimation(),rh(l,{shape:{stackedOnPoints:f}},u),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var y=[],v=h.status,m=0;me&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n10&&"cartesian2d"===o.type&&r){var s=o.getBaseAxis(),l=o.getOtherAxis(s),u=s.getExtent(),h=n.getDevicePixelRatio(),c=Math.abs(u[1]-u[0])*(h||1),p=Math.round(a/c);if(isFinite(p)&&p>1){"lttb"===r&&t.setData(i.lttbDownSample(i.mapDimension(l.dim),1/p));var d=void 0;X(r)?d=wS[r]:U(r)&&(d=r),d&&t.setData(i.downSample(i.mapDimension(l.dim),1/p,d,SS))}}}}}var IS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(t,e){return rx(null,this,{useEncodeDefaulter:!0})},e.prototype.getMarkerPosition=function(t){var e=this.coordinateSystem;if(e&&e.clampData){var n=e.dataToPoint(e.clampData(t)),i=this.getData(),r=i.getLayout("offset"),o=i.getLayout("size");return n[e.getBaseAxis().isHorizontal()?0:1]+=r+o/2,n}return[NaN,NaN]},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},e}(sg);sg.registerClass(IS);var TS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(){return rx(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},e.prototype.getProgressiveThreshold=function(){var t=this.get("progressiveThreshold"),e=this.get("largeThreshold");return e>t&&(t=e),t},e.prototype.brushSelector=function(t,e,n){return n.rect(e.getItemLayout(t))},e.type="series.bar",e.dependencies=["grid","polar"],e.defaultOption=yc(IS.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),e}(IS),CS=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},DS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="sausage",n}return n(e,t),e.prototype.getDefaultShape=function(){return new CS},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=.5*(o-r),s=r+a,l=e.startAngle,u=e.endAngle,h=e.clockwise,c=2*Math.PI,p=h?u-lo)return!0;o=u}return!1},e.prototype._isOrderDifferentInView=function(t,e){for(var n=e.scale,i=n.getExtent(),r=Math.max(0,i[0]),o=Math.min(i[1],n.getOrdinalMeta().categories.length-1);r<=o;++r)if(t.ordinalNumbers[r]!==n.getRawOrdinalNumber(r))return!0},e.prototype._updateSortWithinSameData=function(t,e,n,i){if(this._isOrderChangedWithinSameData(t,e,n)){var r=this._dataSort(t,n,e);this._isOrderDifferentInView(r,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:r}))}},e.prototype._dispatchInitSort=function(t,e,n){var i=e.baseAxis,r=this._dataSort(t,i,(function(n){return t.get(t.mapDimension(e.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:r})},e.prototype.remove=function(t,e){this._clear(this._model),this._removeOnRenderedListener(e)},e.prototype.dispose=function(t,e){this._removeOnRenderedListener(e)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var e=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(e){uh(e,t,Hs(e).dataIndex)}))):e.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type="bar",e}(xg),RS={cartesian2d:function(t,e){var n=e.width<0?-1:1,i=e.height<0?-1:1;n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height);var r=t.x+t.width,o=t.y+t.height,a=LS(e.x,t.x),s=PS(e.x+e.width,r),l=LS(e.y,t.y),u=PS(e.y+e.height,o),h=sr?s:a,e.y=c&&l>o?u:l,e.width=h?0:s-a,e.height=c?0:u-l,n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height),h||c},polar:function(t,e){var n=e.r0<=e.r?1:-1;if(n<0){var i=e.r;e.r=e.r0,e.r0=i}var r=PS(e.r,t.r),o=LS(e.r0,t.r0);e.r=r,e.r0=o;var a=r-o<0;if(n<0){i=e.r;e.r=e.r0,e.r0=i}return a}},NS={cartesian2d:function(t,e,n,i,r,o,a,s,l){var u=new Cs({shape:A({},i),z2:1});(u.__dataIndex=n,u.name="item",o)&&(u.shape[r?"height":"width"]=0);return u},polar:function(t,e,n,i,r,o,a,s,l){var u=!r&&l?DS:Cu,h=new u({shape:i,z2:1});h.name="item";var c,p,d=WS(r);if(h.calculateTextPosition=(c=d,p=({isRoundCap:u===DS}||{}).isRoundCap,function(t,e,n){var i=e.position;if(!i||i instanceof Array)return yr(t,e,n);var r=c(i),o=null!=e.distance?e.distance:5,a=this.shape,s=a.cx,l=a.cy,u=a.r,h=a.r0,d=(u+h)/2,f=a.startAngle,g=a.endAngle,y=(f+g)/2,v=p?Math.abs(u-h)/2:0,m=Math.cos,x=Math.sin,_=s+u*m(f),b=l+u*x(f),w="left",S="top";switch(r){case"startArc":_=s+(h-o)*m(y),b=l+(h-o)*x(y),w="center",S="top";break;case"insideStartArc":_=s+(h+o)*m(y),b=l+(h+o)*x(y),w="center",S="bottom";break;case"startAngle":_=s+d*m(f)+AS(f,o+v,!1),b=l+d*x(f)+kS(f,o+v,!1),w="right",S="middle";break;case"insideStartAngle":_=s+d*m(f)+AS(f,-o+v,!1),b=l+d*x(f)+kS(f,-o+v,!1),w="left",S="middle";break;case"middle":_=s+d*m(y),b=l+d*x(y),w="center",S="middle";break;case"endArc":_=s+(u+o)*m(y),b=l+(u+o)*x(y),w="center",S="bottom";break;case"insideEndArc":_=s+(u-o)*m(y),b=l+(u-o)*x(y),w="center",S="top";break;case"endAngle":_=s+d*m(g)+AS(g,o+v,!0),b=l+d*x(g)+kS(g,o+v,!0),w="left",S="middle";break;case"insideEndAngle":_=s+d*m(g)+AS(g,-o+v,!0),b=l+d*x(g)+kS(g,-o+v,!0),w="right",S="middle";break;default:return yr(t,e,n)}return(t=t||{}).x=_,t.y=b,t.align=w,t.verticalAlign=S,t}),o){var f=r?"r":"endAngle",g={};h.shape[f]=r?0:i.startAngle,g[f]=i[f],(s?rh:oh)(h,{shape:g},o)}return h}};function ES(t,e,n,i,r,o,a,s){var l,u;o?(u={x:i.x,width:i.width},l={y:i.y,height:i.height}):(u={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(a?rh:oh)(n,{shape:l},e,r,null),(a?rh:oh)(n,{shape:u},e?t.baseAxis.model:null,r)}function zS(t,e){for(var n=0;n0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function WS(t){return function(t){var e=t?"Arc":"Angle";return function(t){switch(t){case"start":case"insideStart":case"end":case"insideEnd":return t+e;default:return t}}}(t)}function HS(t,e,n,i,r,o,a,s){var l=e.getItemVisual(n,"style");s||t.setShape("r",i.get(["itemStyle","borderRadius"])||0),t.useStyle(l);var u=i.getShallow("cursor");u&&t.attr("cursor",u);var h=s?a?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":a?r.height>=0?"bottom":"top":r.width>=0?"right":"left",c=Yh(i);Hh(t,c,{labelFetcher:o,labelDataIndex:n,defaultText:Ww(o.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:h});var p=t.getTextContent();if(s&&p){var d=i.get(["label","position"]);t.textConfig.inside="middle"===d||null,function(t,e,n,i){if(j(i))t.setTextConfig({rotation:i});else if(Y(e))t.setTextConfig({rotation:0});else{var r,o=t.shape,a=o.clockwise?o.startAngle:o.endAngle,s=o.clockwise?o.endAngle:o.startAngle,l=(a+s)/2,u=n(e);switch(u){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":r=l;break;case"startAngle":case"insideStartAngle":r=a;break;case"endAngle":case"insideEndAngle":r=s;break;default:return void t.setTextConfig({rotation:0})}var h=1.5*Math.PI-r;"middle"===u&&h>Math.PI/2&&h<1.5*Math.PI&&(h-=Math.PI),t.setTextConfig({rotation:h})}}(t,"outside"===d?h:d,WS(a),i.get(["label","rotate"]))}Qh(p,c,o.getRawValue(n),(function(t){return Hw(e,t)}));var f=i.getModel(["emphasis"]);Rl(t,f.get("focus"),f.get("blurScope"),f.get("disabled")),Vl(t,i),function(t){return null!=t.startAngle&&null!=t.endAngle&&t.startAngle===t.endAngle}(r)&&(t.style.fill="none",t.style.stroke="none",E(t.states,(function(t){t.style&&(t.style.fill=t.style.stroke="none")})))}var YS=function(){},US=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeBar",n}return n(e,t),e.prototype.getDefaultShape=function(){return new YS},e.prototype.buildPath=function(t,e){for(var n=e.points,i=this.baseDimIdx,r=1-this.baseDimIdx,o=[],a=[],s=this.barWidth,l=0;l=s[0]&&e<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return a[h]}return-1}(this,t.offsetX,t.offsetY);Hs(this).dataIndex=e>=0?e:null}),30,!1);function jS(t,e,n){if(uS(n,"cartesian2d")){var i=e,r=n.getArea();return{x:t?i.x:r.x,y:t?r.y:i.y,width:t?i.width:r.width,height:t?r.height:i.height}}var o=e;return{cx:(r=n.getArea()).cx,cy:r.cy,r0:t?r.r0:o.r0,r:t?r.r:o.r,startAngle:t?o.startAngle:0,endAngle:t?o.endAngle:2*Math.PI}}var qS=2*Math.PI,KS=Math.PI/180;function $S(t,e){return xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function JS(t,e){var n=$S(t,e),i=t.get("center"),r=t.get("radius");Y(r)||(r=[0,r]),Y(i)||(i=[i,i]);var o=Er(n.width,e.getWidth()),a=Er(n.height,e.getHeight()),s=Math.min(o,a);return{cx:Er(i[0],o)+n.x,cy:Er(i[1],a)+n.y,r0:Er(r[0],s/2),r:Er(r[1],s/2)}}function QS(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.getData(),i=e.mapDimension("value"),r=$S(t,n),o=JS(t,n),a=o.cx,s=o.cy,l=o.r,u=o.r0,h=-t.get("startAngle")*KS,c=t.get("minAngle")*KS,p=0;e.each(i,(function(t){!isNaN(t)&&p++}));var d=e.getSum(i),f=Math.PI/(d||p)*2,g=t.get("clockwise"),y=t.get("roseType"),v=t.get("stillShowZeroSum"),m=e.getDataExtent(i);m[0]=0;var x=qS,_=0,b=h,w=g?1:-1;if(e.setLayout({viewRect:r,r:l}),e.each(i,(function(t,n){var i;if(isNaN(t))e.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:g,cx:a,cy:s,r0:u,r:y?NaN:l});else{(i="area"!==y?0===d&&v?f:t*f:qS/p)n?a:o,h=Math.abs(l.label.y-n);if(h>=u.maxY){var c=l.label.x-e-l.len2*r,p=i+l.len,f=Math.abs(c)t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function rM(t){return"center"===t.position}function oM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*eM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=Er(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=Er(S,u);var M=w.get("length2");if(M=Er(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var U=(p.style.margin||0)+2.1;Y.y-=U/2,Y.height+=U,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new Ji(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u=n.r0}},e.type="pie",e}(xg);function uM(t,e,n){e=Y(e)&&{coordDimensions:e}||A({encodeDefine:t.getEncode()},e);var i=t.getSource(),r=Km(i,e).dimensions,o=new qm(r,t);return o.initData(i,n),o}var hM=function(){function t(t,e){this._getDataWithEncodedVisual=t,this._getRawData=e}return t.prototype.getAllNames=function(){var t=this._getRawData();return t.mapArray(t.getName)},t.prototype.containName=function(t){return this._getRawData().indexOfName(t)>=0},t.prototype.indexOfName=function(t){return this._getDataWithEncodedVisual().indexOfName(t)},t.prototype.getItemVisual=function(t,e){return this._getDataWithEncodedVisual().getItemVisual(t,e)},t}(),cM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new hM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.mergeOption=function(){t.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return uM(this,{coordDimensions:["value"],encodeDefaulter:H(Yp,this)})},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=[];return n.each(n.mapDimension("value"),(function(t){r.push(t)})),i.percent=Wr(r,e,n.hostModel.get("percentPrecision")),i.$vars.push("percent"),i},e.prototype._defaultLabelLine=function(t){co(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series.pie",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(sg);var pM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){return rx(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},e.prototype.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}},e}(sg),dM=function(){},fM=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.getDefaultShape=function(){return new dM},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.buildPath=function(t,e){var n,i=e.points,r=e.size,o=this.symbolProxy,a=o.shape,s=t.getContext?t.getContext():t,l=s&&r[0]<4,u=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,u=i[l]-o/2,h=i[l+1]-a/2;if(t>=u&&e>=h&&t<=u+o&&e<=h+a)return s}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape,n=e.points,i=e.size,r=i[0],o=i[1],a=1/0,s=1/0,l=-1/0,u=-1/0,h=0;h=0&&(l.dataIndex=n+(t.startIndex||0))}))},t.prototype.remove=function(){this._clear()},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),yM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i,{clipShape:this._getClipShape(t)}),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData(),{clipShape:this._getClipShape(e)}),this._finished=t.end===e.getData().count()},e.prototype.updateTransform=function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var r=bS("").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._getClipShape=function(t){var e=t.coordinateSystem,n=e&&e.getArea&&e.getArea();return t.get("clip",!0)?n:null},e.prototype._updateSymbolDraw=function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new gM:new qw,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(xg),vM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},e}(Tp),mM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Co).models[0]},e.type="cartesian2dAxis",e}(Tp);R(mM,p_);var xM={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},_M=C({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},xM),bM=C({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},xM),wM={category:_M,value:bM,time:C({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},bM),log:k({logBase:10},bM)},SM={value:1,category:1,time:1,log:1};function MM(t,e,i,r){E(SM,(function(o,a){var s=C(C({},wM[a],!0),r,!0),l=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e+"Axis."+a,n}return n(i,t),i.prototype.mergeDefaultAndTheme=function(t,e){var n=bp(this),i=n?Sp(t):{};C(t,e.getTheme().get(a+"Axis")),C(t,this.getDefaultOption()),t.type=IM(t),n&&wp(t,i,n)},i.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=sx.createByAxisModel(this))},i.prototype.getCategories=function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},i.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},i.type=e+"Axis."+a,i.defaultOption=s,i}(i);t.registerComponentModel(l)})),t.registerSubTypeDefaulter(e+"Axis",IM)}function IM(t){return t.type||(t.data?"category":"value")}var TM=function(){function t(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return t.prototype.getAxis=function(t){return this._axes[t]},t.prototype.getAxes=function(){return z(this._dimList,(function(t){return this._axes[t]}),this)},t.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),B(this.getAxes(),(function(e){return e.scale.type===t}))},t.prototype.addAxis=function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},t}(),CM=["x","y"];function DM(t){return"interval"===t.type||"time"===t.type}var AM=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="cartesian2d",e.dimensions=CM,e}return n(e,t),e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,e=this.getAxis("y").scale;if(DM(t)&&DM(e)){var n=t.getExtent(),i=e.getExtent(),r=this.dataToPoint([n[0],i[0]]),o=this.dataToPoint([n[1],i[1]]),a=n[1]-n[0],s=i[1]-i[0];if(a&&s){var l=(o[0]-r[0])/a,u=(o[1]-r[1])/s,h=r[0]-n[0]*l,c=r[1]-i[0]*u,p=this._transform=[l,0,0,u,h,c];this._invTransform=Bi([],p)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,e){var n=this.dataToPoint(t),i=this.dataToPoint(e),r=this.getArea(),o=new sr(n[0],n[1],i[0]-n[0],i[1]-n[1]);return r.intersect(o)},e.prototype.dataToPoint=function(t,e,n){n=n||[];var i=t[0],r=t[1];if(this._transform&&null!=i&&isFinite(i)&&null!=r&&isFinite(r))return Ft(n,t,this._transform);var o=this.getAxis("x"),a=this.getAxis("y");return n[0]=o.toGlobalCoord(o.dataToCoord(i,e)),n[1]=a.toGlobalCoord(a.dataToCoord(r,e)),n},e.prototype.clampData=function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return(e=e||[])[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},e.prototype.pointToData=function(t,e){var n=[];if(this._invTransform)return Ft(n,t,this._invTransform);var i=this.getAxis("x"),r=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),e),n[1]=r.coordToData(r.toLocalCoord(t[1]),e),n},e.prototype.getOtherAxis=function(t){return this.getAxis("x"===t.dim?"y":"x")},e.prototype.getArea=function(){var t=this.getAxis("x").getGlobalExtent(),e=this.getAxis("y").getGlobalExtent(),n=Math.min(t[0],t[1]),i=Math.min(e[0],e[1]),r=Math.max(t[0],t[1])-n,o=Math.max(e[0],e[1])-i;return new sr(n,i,r,o)},e}(TM),kM=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.index=0,a.type=r||"value",a.position=o||"bottom",a}return n(e,t),e.prototype.isHorizontal=function(){var t=this.position;return"top"===t||"bottom"===t},e.prototype.getGlobalExtent=function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},e.prototype.pointToData=function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},e.prototype.setCategorySortInfo=function(t){if("category"!==this.type)return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(H_);function LM(t,e,n){n=n||{};var i=t.coordinateSystem,r=e.axis,o={},a=r.getAxesOnZeroOf()[0],s=r.position,l=a?"onZero":s,u=r.dim,h=i.getRect(),c=[h.x,h.x+h.width,h.y,h.y+h.height],p={left:0,right:1,top:0,bottom:1,onZero:2},d=e.get("offset")||0,f="x"===u?[c[2]-d,c[3]+d]:[c[0]-d,c[1]+d];if(a){var g=a.toGlobalCoord(a.dataToCoord(0));f[p.onZero]=Math.max(Math.min(g,f[1]),f[0])}o.position=["y"===u?f[p[l]]:c[0],"x"===u?f[p[l]]:c[3]],o.rotation=Math.PI/2*("x"===u?0:1);o.labelDirection=o.tickDirection=o.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],o.labelOffset=a?f[p[s]]-f[p.onZero]:0,e.get(["axisTick","inside"])&&(o.tickDirection=-o.tickDirection),it(n.labelInside,e.get(["axisLabel","inside"]))&&(o.labelDirection=-o.labelDirection);var y=e.get(["axisLabel","rotate"]);return o.labelRotate="top"===l?-y:y,o.z2=1,o}function PM(t){return"cartesian2d"===t.get("coordinateSystem")}function OM(t){var e={xAxisModel:null,yAxisModel:null};return E(e,(function(n,i){var r=i.replace(/Model$/,""),o=t.getReferringComponents(r,Co).models[0];e[i]=o})),e}var RM=Math.log;function NM(t,e,n){var i=xx.prototype,r=i.getTicks.call(n),o=i.getTicks.call(n,!0),a=r.length-1,s=i.getInterval.call(n),l=i_(t,e),u=l.extent,h=l.fixMin,c=l.fixMax;if("log"===t.type){var p=RM(t.base);u=[RM(u[0])/p,RM(u[1])/p]}t.setExtent(u[0],u[1]),t.calcNiceExtent({splitNumber:a,fixMin:h,fixMax:c});var d=i.getExtent.call(t);h&&(u[0]=d[0]),c&&(u[1]=d[1]);var f=i.getInterval.call(t),g=u[0],y=u[1];if(h&&c)f=(y-g)/a;else if(h)for(y=u[0]+f*a;yu[0]&&isFinite(g)&&isFinite(u[0]);)f=cx(f),g=u[1]-f*a;else{t.getTicks().length-1>a&&(f=cx(f));var v=f*a;(g=zr((y=Math.ceil(u[1]/f)*f)-v))<0&&u[0]>=0?(g=0,y=zr(v)):y>0&&u[1]<=0&&(y=0,g=-zr(v))}var m=(r[0].value-o[0].value)/s,x=(r[a].value-o[a].value)/s;i.setExtent.call(t,g+f*m,y+f*x),i.setInterval.call(t,f),(m||x)&&i.setNiceExtent.call(t,g+f,y-f)}var EM=function(){function t(t,e,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=CM,this._initCartesian(t,e,n),this.model=t}return t.prototype.getRect=function(){return this._rect},t.prototype.update=function(t,e){var n=this._axesMap;function i(t){var e,n=G(t),i=n.length;if(i){for(var r=[],o=i-1;o>=0;o--){var a=t[+n[o]],s=a.model,l=a.scale;ux(l)&&s.get("alignTicks")&&null==s.get("interval")?r.push(a):(r_(l,s),ux(l)&&(e=a))}r.length&&(e||r_((e=r.pop()).scale,e.model),E(r,(function(t){NM(t.scale,t.model,e.scale)})))}}this._updateScale(t,this.model),i(n.x),i(n.y);var r={};E(n.x,(function(t){VM(n,"y",t,r)})),E(n.y,(function(t){VM(n,"x",t,r)})),this.resize(this.model,e)},t.prototype.resize=function(t,e,n){var i=t.getBoxLayoutParams(),r=!n&&t.get("containLabel"),o=xp(i,{width:e.getWidth(),height:e.getHeight()});this._rect=o;var a=this._axesList;function s(){E(a,(function(t){var e=t.isHorizontal(),n=e?[0,o.width]:[0,o.height],i=t.inverse?1:0;t.setExtent(n[i],n[1-i]),function(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}(t,e?o.x:o.y)}))}s(),r&&(E(a,(function(t){if(!t.model.get(["axisLabel","inside"])){var e=function(t){var e=t.model,n=t.scale;if(e.get(["axisLabel","show"])&&!n.isBlank()){var i,r,o=n.getExtent();r=n instanceof vx?n.count():(i=n.getTicks()).length;var a,s=t.getLabelModel(),l=a_(t),u=1;r>40&&(u=Math.ceil(r/40));for(var h=0;h0&&i>0||n<0&&i<0)}(t)}var FM=Math.PI,GM=function(){function t(t,e){this.group=new Cr,this.opt=e,this.axisModel=t,k(e,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new Cr({x:e.position[0],y:e.position[1],rotation:e.rotation});n.updateTransform(),this._transformGroup=n}return t.prototype.hasBuilder=function(t){return!!WM[t]},t.prototype.add=function(t){WM[t](this.opt,this.axisModel,this.group,this._transformGroup)},t.prototype.getGroup=function(){return this.group},t.innerTextLayout=function(t,e,n){var i,r,o=Ur(e-t);return Xr(o)?(r=n>0?"top":"bottom",i="center"):Xr(o-FM)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},t.makeAxisEventDataBase=function(t){var e={componentType:t.mainType,componentIndex:t.componentIndex};return e[t.mainType+"Index"]=t.componentIndex,e},t.isLabelSilent=function(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)},t}(),WM={axisLine:function(t,e,n,i){var r=e.get(["axisLine","show"]);if("auto"===r&&t.handleAutoShown&&(r=t.handleAutoShown("axisLine")),r){var o=e.axis.getExtent(),a=i.transform,s=[o[0],0],l=[o[1],0];a&&(Ft(s,s,a),Ft(l,l,a));var u=A({lineCap:"round"},e.getModel(["axisLine","lineStyle"]).getLineStyle()),h=new zu({subPixelOptimize:!0,shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:u,strokeContainThreshold:t.strokeContainThreshold||5,silent:!0,z2:1});h.anid="line",n.add(h);var c=e.get(["axisLine","symbol"]);if(null!=c){var p=e.get(["axisLine","symbolSize"]);X(c)&&(c=[c,c]),(X(p)||j(p))&&(p=[p,p]);var d=Oy(e.get(["axisLine","symbolOffset"])||0,p),f=p[0],g=p[1];E([{rotate:t.rotation+Math.PI/2,offset:d[0],r:0},{rotate:t.rotation-Math.PI/2,offset:d[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(e,i){if("none"!==c[i]&&null!=c[i]){var r=Ly(c[i],-f/2,-g/2,f,g,u.stroke,!0),o=e.r+e.offset;r.attr({rotation:e.rotate,x:s[0]+o*Math.cos(t.rotation),y:s[1]-o*Math.sin(t.rotation),silent:!0,z2:11}),n.add(r)}}))}}},axisTickLabel:function(t,e,n,i){var r=function(t,e,n,i){var r=n.axis,o=n.getModel("axisTick"),a=o.get("show");"auto"===a&&i.handleAutoShown&&(a=i.handleAutoShown("axisTick"));if(!a||r.scale.isBlank())return;for(var s=o.getModel("lineStyle"),l=i.tickDirection*o.get("length"),u=XM(r.getTicksCoords(),e.transform,l,k(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),h=0;hc[1]?-1:1,d=["start"===s?c[0]-p*h:"end"===s?c[1]+p*h:(c[0]+c[1])/2,UM(s)?t.labelOffset+l*h:0],f=e.get("nameRotate");null!=f&&(f=f*FM/180),UM(s)?o=GM.innerTextLayout(t.rotation,null!=f?f:t.rotation,l):(o=function(t,e,n,i){var r,o,a=Ur(n-t),s=i[0]>i[1],l="start"===e&&!s||"start"!==e&&s;Xr(a-FM/2)?(o=l?"bottom":"top",r="center"):Xr(a-1.5*FM)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*FM&&a>FM/2?l?"left":"right":l?"right":"left");return{rotation:a,textAlign:r,textVerticalAlign:o}}(t.rotation,s,f||0,c),null!=(a=t.axisNameAvailableWidth)&&(a=Math.abs(a/Math.sin(o.rotation)),!isFinite(a)&&(a=null)));var g=u.getFont(),y=e.get("nameTruncate",!0)||{},v=y.ellipsis,m=it(t.nameTruncateMaxWidth,y.maxWidth,a),x=new ks({x:d[0],y:d[1],rotation:o.rotation,silent:GM.isLabelSilent(e),style:Uh(u,{text:r,font:g,overflow:"truncate",width:m,ellipsis:v,fill:u.getTextColor()||e.get(["axisLine","lineStyle","color"]),align:u.get("align")||o.textAlign,verticalAlign:u.get("verticalAlign")||o.textVerticalAlign}),z2:1});if(Eh({el:x,componentModel:e,itemName:r}),x.__fullText=r,x.anid="name",e.get("triggerEvent")){var _=GM.makeAxisEventDataBase(e);_.targetType="axisName",_.name=r,Hs(x).eventData=_}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function HM(t){t&&(t.ignore=!0)}function YM(t,e){var n=t&&t.getBoundingRect().clone(),i=e&&e.getBoundingRect().clone();if(n&&i){var r=Oi([]);return zi(r,r,-t.rotation),n.applyTransform(Ni([],r,t.getLocalTransform())),i.applyTransform(Ni([],r,e.getLocalTransform())),n.intersect(i)}}function UM(t){return"middle"===t||"center"===t}function XM(t,e,n,i,r){for(var o=[],a=[],s=[],l=0;l=0||t===e}function qM(t){var e=KM(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=$M(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==a||a>l[1])&&(a=l[1]),a0&&!c.min?c.min=0:null!=c.min&&c.min<0&&!c.max&&(c.max=0);var p=a;null!=c.color&&(p=k({color:c.color},a));var d=C(T(c),{boundaryGap:t,splitNumber:e,scale:n,axisLine:i,axisTick:r,axisLabel:o,name:c.text,showName:s,nameLocation:"end",nameGap:u,nameTextStyle:p,triggerEvent:h},!1);if(X(l)){var f=d.name;d.name=l.replace("{value}",null!=f?f:"")}else U(l)&&(d.name=l(d.name,d));var g=new dc(d,null,this.ecModel);return R(g,p_.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=c},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type="radar",e.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:C({lineStyle:{color:"#bbb"}},xI.axisLine),axisLabel:_I(xI.axisLabel,!1),axisTick:_I(xI.axisTick,!1),splitLine:_I(xI.splitLine,!0),splitArea:_I(xI.splitArea,!0),indicator:[]},e}(Tp),wI=["axisLine","axisTickLabel","axisName"],SI=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll(),this._buildAxes(t),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t){var e=t.coordinateSystem;E(z(e.getIndicatorAxes(),(function(t){var n=t.model.get("showName")?t.name:"";return new GM(t.model,{axisName:n,position:[e.cx,e.cy],rotation:t.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(t){E(wI,t.add,t),this.group.add(t.getGroup())}),this)},e.prototype._buildSplitLineAndArea=function(t){var e=t.coordinateSystem,n=e.getIndicatorAxes();if(n.length){var i=t.get("shape"),r=t.getModel("splitLine"),o=t.getModel("splitArea"),a=r.getModel("lineStyle"),s=o.getModel("areaStyle"),l=r.get("show"),u=o.get("show"),h=a.get("color"),c=s.get("color"),p=Y(h)?h:[h],d=Y(c)?c:[c],f=[],g=[];if("circle"===i)for(var y=n[0].getTicksCoords(),v=e.cx,m=e.cy,x=0;x3?1.4:r>1?1.2:1.1;LI(this,"zoom","zoomOnMouseWheel",t,{scale:i>0?s:1/s,originX:o,originY:a,isAvailableBehavior:null})}if(n){var l=Math.abs(i);LI(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:o,originY:a,isAvailableBehavior:null})}}},e.prototype._pinchHandler=function(t){DI(this._zr,"globalPan")||LI(this,"zoom",null,t,{scale:t.pinchScale>1?1.1:1/1.1,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})},e}(Xt);function LI(t,e,n,i,r){t.pointerChecker&&t.pointerChecker(i,r.originX,r.originY)&&(se(i.event),PI(t,e,n,i,r))}function PI(t,e,n,i,r){r.isAvailableBehavior=W(OI,null,n,i),t.trigger(e,r)}function OI(t,e,n){var i=n[t];return!t||i&&(!X(i)||e.event[i+"Key"])}function RI(t,e,n){var i=t.target;i.x+=e,i.y+=n,i.dirty()}function NI(t,e,n,i){var r=t.target,o=t.zoomLimit,a=t.zoom=t.zoom||1;if(a*=e,o){var s=o.min||0,l=o.max||1/0;a=Math.max(Math.min(l,a),s)}var u=a/t.zoom;t.zoom=a,r.x-=(n-r.x)*(u-1),r.y-=(i-r.y)*(u-1),r.scaleX*=u,r.scaleY*=u,r.dirty()}var EI,zI={axisPointer:1,tooltip:1,brush:1};function VI(t,e,n){var i=e.getComponentByElement(t.topTarget),r=i&&i.coordinateSystem;return i&&i!==n&&!zI.hasOwnProperty(i.mainType)&&r&&r.model!==n}function BI(t){X(t)&&(t=(new DOMParser).parseFromString(t,"text/xml"));var e=t;for(9===e.nodeType&&(e=e.firstChild);"svg"!==e.nodeName.toLowerCase()||1!==e.nodeType;)e=e.nextSibling;return e}var FI={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},GI=G(FI),WI={"alignment-baseline":"textBaseline","stop-color":"stopColor"},HI=G(WI),YI=function(){function t(){this._defs={},this._root=null}return t.prototype.parse=function(t,e){e=e||{};var n=BI(t);this._defsUsePending=[];var i=new Cr;this._root=i;var r=[],o=n.getAttribute("viewBox")||"",a=parseFloat(n.getAttribute("width")||e.width),s=parseFloat(n.getAttribute("height")||e.height);isNaN(a)&&(a=null),isNaN(s)&&(s=null),KI(n,i,null,!0,!1);for(var l,u,h=n.firstChild;h;)this._parseNode(h,i,r,null,!1,!1),h=h.nextSibling;if(function(t,e){for(var n=0;n=4&&(l={x:parseFloat(c[0]||0),y:parseFloat(c[1]||0),width:parseFloat(c[2]),height:parseFloat(c[3])})}if(l&&null!=a&&null!=s&&(u=oT(l,{x:0,y:0,width:a,height:s}),!e.ignoreViewBox)){var p=i;(i=new Cr).add(p),p.scaleX=p.scaleY=u.scale,p.x=u.x,p.y=u.y}return e.ignoreRootClip||null==a||null==s||i.setClipPath(new Cs({shape:{x:0,y:0,width:a,height:s}})),{root:i,width:a,height:s,viewBoxRect:l,viewBoxTransform:u,named:r}},t.prototype._parseNode=function(t,e,n,i,r,o){var a,s=t.nodeName.toLowerCase(),l=i;if("defs"===s&&(r=!0),"text"===s&&(o=!0),"defs"===s||"switch"===s)a=e;else{if(!r){var u=EI[s];if(u&&mt(EI,s)){a=u.call(this,t,e);var h=t.getAttribute("name");if(h){var c={name:h,namedFrom:null,svgNodeTagLower:s,el:a};n.push(c),"g"===s&&(l=c)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:a});e.add(a)}}var p=UI[s];if(p&&mt(UI,s)){var d=p.call(this,t),f=t.getAttribute("id");f&&(this._defs[f]=d)}}if(a&&a.isGroup)for(var g=t.firstChild;g;)1===g.nodeType?this._parseNode(g,a,n,l,r,o):3===g.nodeType&&o&&this._parseText(g,a),g=g.nextSibling},t.prototype._parseText=function(t,e){var n=new vs({style:{text:t.textContent},silent:!0,x:this._textX||0,y:this._textY||0});jI(e,n),KI(t,n,this._defsUsePending,!1,!1),function(t,e){var n=e.__selfStyle;if(n){var i=n.textBaseline,r=i;i&&"auto"!==i?"baseline"===i?r="alphabetic":"before-edge"===i||"text-before-edge"===i?r="top":"after-edge"===i||"text-after-edge"===i?r="bottom":"central"!==i&&"mathematical"!==i||(r="middle"):r="alphabetic",t.style.textBaseline=r}var o=e.__inheritedStyle;if(o){var a=o.textAlign,s=a;a&&("middle"===a&&(s="center"),t.style.textAlign=s)}}(n,e);var i=n.style,r=i.fontSize;r&&r<9&&(i.fontSize=9,n.scaleX*=r/9,n.scaleY*=r/9);var o=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=o;var a=n.getBoundingRect();return this._textX+=a.width,e.add(n),n},t.internalField=void(EI={g:function(t,e){var n=new Cr;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n},rect:function(t,e){var n=new Cs;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(t.getAttribute("x")||"0"),y:parseFloat(t.getAttribute("y")||"0"),width:parseFloat(t.getAttribute("width")||"0"),height:parseFloat(t.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(t,e){var n=new hu;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),r:parseFloat(t.getAttribute("r")||"0")}),n.silent=!0,n},line:function(t,e){var n=new zu;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(t.getAttribute("x1")||"0"),y1:parseFloat(t.getAttribute("y1")||"0"),x2:parseFloat(t.getAttribute("x2")||"0"),y2:parseFloat(t.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(t,e){var n=new pu;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),rx:parseFloat(t.getAttribute("rx")||"0"),ry:parseFloat(t.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(t,e){var n,i=t.getAttribute("points");i&&(n=qI(i));var r=new Pu({shape:{points:n||[]},silent:!0});return jI(e,r),KI(t,r,this._defsUsePending,!1,!1),r},polyline:function(t,e){var n,i=t.getAttribute("points");i&&(n=qI(i));var r=new Ru({shape:{points:n||[]},silent:!0});return jI(e,r),KI(t,r,this._defsUsePending,!1,!1),r},image:function(t,e){var n=new _s;return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.setStyle({image:t.getAttribute("xlink:href")||t.getAttribute("href"),x:+t.getAttribute("x"),y:+t.getAttribute("y"),width:+t.getAttribute("width"),height:+t.getAttribute("height")}),n.silent=!0,n},text:function(t,e){var n=t.getAttribute("x")||"0",i=t.getAttribute("y")||"0",r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(r),this._textY=parseFloat(i)+parseFloat(o);var a=new Cr;return jI(e,a),KI(t,a,this._defsUsePending,!1,!0),a},tspan:function(t,e){var n=t.getAttribute("x"),i=t.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0",a=new Cr;return jI(e,a),KI(t,a,this._defsUsePending,!1,!0),this._textX+=parseFloat(r),this._textY+=parseFloat(o),a},path:function(t,e){var n=su(t.getAttribute("d")||"");return jI(e,n),KI(t,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),t}(),UI={lineargradient:function(t){var e=parseInt(t.getAttribute("x1")||"0",10),n=parseInt(t.getAttribute("y1")||"0",10),i=parseInt(t.getAttribute("x2")||"10",10),r=parseInt(t.getAttribute("y2")||"0",10),o=new Xu(e,n,i,r);return XI(t,o),ZI(t,o),o},radialgradient:function(t){var e=parseInt(t.getAttribute("cx")||"0",10),n=parseInt(t.getAttribute("cy")||"0",10),i=parseInt(t.getAttribute("r")||"0",10),r=new Zu(e,n,i);return XI(t,r),ZI(t,r),r}};function XI(t,e){"userSpaceOnUse"===t.getAttribute("gradientUnits")&&(e.global=!0)}function ZI(t,e){for(var n=t.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),r=void 0;r=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var o={};rT(n,o,o);var a=o.stopColor||n.getAttribute("stop-color")||"#000000";e.colorStops.push({offset:r,color:a})}n=n.nextSibling}}function jI(t,e){t&&t.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),k(e.__inheritedStyle,t.__inheritedStyle))}function qI(t){for(var e=tT(t),n=[],i=0;i0;o-=2){var a=i[o],s=i[o-1],l=tT(a);switch(r=r||[1,0,0,1,0,0],s){case"translate":Ei(r,r,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":Vi(r,r,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":zi(r,r,-parseFloat(l[0])*nT);break;case"skewX":Ni(r,[1,0,Math.tan(parseFloat(l[0])*nT),1,0,0],r);break;case"skewY":Ni(r,[1,Math.tan(parseFloat(l[0])*nT),0,1,0,0],r);break;case"matrix":r[0]=parseFloat(l[0]),r[1]=parseFloat(l[1]),r[2]=parseFloat(l[2]),r[3]=parseFloat(l[3]),r[4]=parseFloat(l[4]),r[5]=parseFloat(l[5])}}e.setLocalTransform(r)}}(t,e),rT(t,a,s),i||function(t,e,n){for(var i=0;i0,f={api:n,geo:s,mapOrGeoModel:t,data:a,isVisualEncodedByVisualMap:d,isGeo:o,transformInfoRaw:c};"geoJSON"===s.resourceType?this._buildGeoJSON(f):"geoSVG"===s.resourceType&&this._buildSVG(f),this._updateController(t,e,n),this._updateMapSelectHandler(t,l,n,i)},t.prototype._buildGeoJSON=function(t){var e=this._regionsGroupByName=ft(),n=ft(),i=this._regionsGroup,r=t.transformInfoRaw,o=t.mapOrGeoModel,a=t.data,s=t.geo.projection,l=s&&s.stream;function u(t,e){return e&&(t=e(t)),t&&[t[0]*r.scaleX+r.x,t[1]*r.scaleY+r.y]}function h(t){for(var e=[],n=!l&&s&&s.project,i=0;i=0)&&(p=r);var d=a?{normal:{align:"center",verticalAlign:"middle"}}:null;Hh(e,Yh(i),{labelFetcher:p,labelDataIndex:c,defaultText:n},d);var f=e.getTextContent();if(f&&(TT(f).ignore=f.ignore,e.textConfig&&a)){var g=e.getBoundingRect().clone();e.textConfig.layoutRect=g,e.textConfig.position=[(a[0]-g.x)/g.width*100+"%",(a[1]-g.y)/g.height*100+"%"]}e.disableLabelAnimation=!0}else e.removeTextContent(),e.removeTextConfig(),e.disableLabelAnimation=null}function PT(t,e,n,i,r,o){t.data?t.data.setItemGraphicEl(o,e):Hs(e).eventData={componentType:"geo",componentIndex:r.componentIndex,geoIndex:r.componentIndex,name:n,region:i&&i.option||{}}}function OT(t,e,n,i,r){t.data||Eh({el:e,componentModel:r,itemName:n,itemTooltipOption:i.get("tooltip")})}function RT(t,e,n,i,r){e.highDownSilentOnTouch=!!r.get("selectedMode");var o=i.getModel("emphasis"),a=o.get("focus");return Rl(e,a,o.get("blurScope"),o.get("disabled")),t.isGeo&&function(t,e,n){var i=Hs(t);i.componentMainType=e.mainType,i.componentIndex=e.componentIndex,i.componentHighDownName=n}(e,r,n),a}function NT(t,e,n){var i,r=[];function o(){i=[]}function a(){i.length&&(r.push(i),i=[])}var s=e({polygonStart:o,polygonEnd:a,lineStart:o,lineEnd:a,point:function(t,e){isFinite(t)&&isFinite(e)&&i.push([t,e])},sphere:function(){}});return!n&&s.polygonStart(),E(t,(function(t){s.lineStart();for(var e=0;e-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},e.type="series.map",e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"},e}(sg);function VT(t){var e={};t.eachSeriesByType("map",(function(t){var n=t.getHostGeoModel(),i=n?"o"+n.id:"i"+t.getMapType();(e[i]=e[i]||[]).push(t)})),E(e,(function(t,e){for(var n,i,r,o=(n=z(t,(function(t){return t.getData()})),i=t[0].get("mapValueCalculation"),r={},E(n,(function(t){t.each(t.mapDimension("value"),(function(e,n){var i="ec-"+t.getName(n);r[i]=r[i]||[],isNaN(e)||r[i].push(e)}))})),n[0].map(n[0].mapDimension("value"),(function(t,e){for(var o="ec-"+n[0].getName(e),a=0,s=1/0,l=-1/0,u=r[o].length,h=0;h1?(d.width=p,d.height=p/x):(d.height=p,d.width=p*x),d.y=c[1]-d.height/2,d.x=c[0]-d.width/2;else{var b=t.getBoxLayoutParams();b.aspect=x,d=xp(b,{width:v,height:m})}this.setViewRect(d.x,d.y,d.width,d.height),this.setCenter(t.get("center"),e),this.setZoom(t.get("zoom"))}R(UT,GT);var jT=new(function(){function t(){this.dimensions=YT}return t.prototype.create=function(t,e){var n=[];function i(t){return{nameProperty:t.get("nameProperty"),aspectScale:t.get("aspectScale"),projection:t.get("projection")}}t.eachComponent("geo",(function(t,r){var o=t.get("map"),a=new UT(o+r,o,A({nameMap:t.get("nameMap")},i(t)));a.zoomLimit=t.get("scaleLimit"),n.push(a),t.coordinateSystem=a,a.model=t,a.resize=ZT,a.resize(t,e)})),t.eachSeries((function(t){if("geo"===t.get("coordinateSystem")){var e=t.get("geoIndex")||0;t.coordinateSystem=n[e]}}));var r={};return t.eachSeriesByType("map",(function(t){if(!t.getHostGeoModel()){var e=t.getMapType();r[e]=r[e]||[],r[e].push(t)}})),E(r,(function(t,r){var o=z(t,(function(t){return t.get("nameMap")})),a=new UT(r,r,A({nameMap:D(o)},i(t[0])));a.zoomLimit=it.apply(null,z(t,(function(t){return t.get("scaleLimit")}))),n.push(a),a.resize=ZT,a.resize(t[0],e),E(t,(function(t){t.coordinateSystem=a,function(t,e){E(e.get("geoCoord"),(function(e,n){t.addGeoCoord(n,e)}))}(a,t)}))})),n},t.prototype.getFilledRegions=function(t,e,n,i){for(var r=(t||[]).slice(),o=ft(),a=0;a=0;){var o=e[n];o.hierNode.prelim+=i,o.hierNode.modifier+=i,r+=o.hierNode.change,i+=o.hierNode.shift+r}}(t);var o=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;r?(t.hierNode.prelim=r.hierNode.prelim+e(t,r),t.hierNode.modifier=t.hierNode.prelim-o):t.hierNode.prelim=o}else r&&(t.hierNode.prelim=r.hierNode.prelim+e(t,r));t.parentNode.hierNode.defaultAncestor=function(t,e,n,i){if(e){for(var r=t,o=t,a=o.parentNode.children[0],s=e,l=r.hierNode.modifier,u=o.hierNode.modifier,h=a.hierNode.modifier,c=s.hierNode.modifier;s=oC(s),o=aC(o),s&&o;){r=oC(r),a=aC(a),r.hierNode.ancestor=t;var p=s.hierNode.prelim+c-o.hierNode.prelim-u+i(s,o);p>0&&(lC(sC(s,t,n),t,p),u+=p,l+=p),c+=s.hierNode.modifier,u+=o.hierNode.modifier,l+=r.hierNode.modifier,h+=a.hierNode.modifier}s&&!oC(r)&&(r.hierNode.thread=s,r.hierNode.modifier+=c-l),o&&!aC(a)&&(a.hierNode.thread=o,a.hierNode.modifier+=u-h,n=t)}return n}(t,r,t.parentNode.hierNode.defaultAncestor||i[0],e)}function nC(t){var e=t.hierNode.prelim+t.parentNode.hierNode.modifier;t.setLayout({x:e},!0),t.hierNode.modifier+=t.parentNode.hierNode.modifier}function iC(t){return arguments.length?t:uC}function rC(t,e){return t-=Math.PI/2,{x:e*Math.cos(t),y:e*Math.sin(t)}}function oC(t){var e=t.children;return e.length&&t.isExpand?e[e.length-1]:t.hierNode.thread}function aC(t){var e=t.children;return e.length&&t.isExpand?e[0]:t.hierNode.thread}function sC(t,e,n){return t.hierNode.ancestor.parentNode===e.parentNode?t.hierNode.ancestor:n}function lC(t,e,n){var i=n/(e.hierNode.i-t.hierNode.i);e.hierNode.change-=i,e.hierNode.shift+=n,e.hierNode.modifier+=n,e.hierNode.prelim+=n,t.hierNode.change+=i}function uC(t,e){return t.parentNode===e.parentNode?1:2}var hC=function(){this.parentPoint=[],this.childPoints=[]},cC=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new hC},e.prototype.buildPath=function(t,e){var n=e.childPoints,i=n.length,r=e.parentPoint,o=n[0],a=n[i-1];if(1===i)return t.moveTo(r[0],r[1]),void t.lineTo(o[0],o[1]);var s=e.orient,l="TB"===s||"BT"===s?0:1,u=1-l,h=Er(e.forkPosition,1),c=[];c[l]=r[l],c[u]=r[u]+(a[u]-r[u])*h,t.moveTo(r[0],r[1]),t.lineTo(c[0],c[1]),t.moveTo(o[0],o[1]),c[l]=o[l],t.lineTo(c[0],c[1]),c[l]=a[l],t.lineTo(c[0],c[1]),t.lineTo(a[0],a[1]);for(var p=1;pm.x)||(_-=Math.PI);var S=b?"left":"right",M=s.getModel("label"),I=M.get("rotate"),T=I*(Math.PI/180),C=y.getTextContent();C&&(y.setTextConfig({position:M.get("position")||S,rotation:null==I?-_:T,origin:"center"}),C.setStyle("verticalAlign","middle"))}var D=s.get(["emphasis","focus"]),A="relative"===D?gt(a.getAncestorsIndices(),a.getDescendantIndices()):"ancestor"===D?a.getAncestorsIndices():"descendant"===D?a.getDescendantIndices():null;A&&(Hs(n).focus=A),function(t,e,n,i,r,o,a,s){var l=e.getModel(),u=t.get("edgeShape"),h=t.get("layout"),c=t.getOrient(),p=t.get(["lineStyle","curveness"]),d=t.get("edgeForkPosition"),f=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===u)e.parentNode&&e.parentNode!==n&&(g||(g=i.__edge=new Gu({shape:mC(h,c,p,r,r)})),rh(g,{shape:mC(h,c,p,o,a)},t));else if("polyline"===u)if("orthogonal"===h){if(e!==n&&e.children&&0!==e.children.length&&!0===e.isExpand){for(var y=e.children,v=[],m=0;me&&(e=i.height)}this.height=e+1},t.prototype.getNodeById=function(t){if(this.getId()===t)return this;for(var e=0,n=this.children,i=n.length;e=0&&this.hostTree.data.setItemLayout(this.dataIndex,t,e)},t.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},t.prototype.setVisual=function(t,e){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,t,e)},t.prototype.getVisual=function(t){return this.hostTree.data.getItemVisual(this.dataIndex,t)},t.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},t.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},t.prototype.getChildIndex=function(){if(this.parentNode){for(var t=this.parentNode.children,e=0;e=0){var i=n.getData().tree.root,r=t.targetNode;if(X(r)&&(r=i.getNodeById(r)),r&&i.contains(r))return{node:r};var o=t.targetNodeId;if(null!=o&&(r=i.getNodeById(o)))return{node:r}}}function LC(t){for(var e=[];t;)(t=t.parentNode)&&e.push(t);return e.reverse()}function PC(t,e){return P(LC(t),e)>=0}function OC(t,e){for(var n=[];t;){var i=t.dataIndex;n.push({name:t.name,dataIndex:i,value:e.getRawValue(i)}),t=t.parentNode}return n.reverse(),n}var RC=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.hasSymbolVisual=!0,e.ignoreStyleOnData=!0,e}return n(e,t),e.prototype.getInitialData=function(t){var e={name:t.name,children:t.data},n=t.leaves||{},i=new dc(n,this,this.ecModel),r=AC.createTree(e,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e);return n&&n.children.length&&n.isExpand||(t.parentModel=i),t}))}));var o=0;r.eachNode("preorder",(function(t){t.depth>o&&(o=t.depth)}));var a=t.expandAndCollapse&&t.initialTreeDepth>=0?t.initialTreeDepth:o;return r.root.eachNode("preorder",(function(t){var e=t.hostTree.data.getRawDataItem(t.dataIndex);t.isExpand=e&&null!=e.collapsed?!e.collapsed:t.depth<=a})),r.data},e.prototype.getOrient=function(){var t=this.get("orient");return"horizontal"===t?t="LR":"vertical"===t&&(t="TB"),t},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData().tree,r=i.root.children[0],o=i.getNodeByDataIndex(t),a=o.getValue(),s=o.name;o&&o!==r;)s=o.parentNode.name+"."+s,o=o.parentNode;return Xf("nameValue",{name:s,value:a,noValue:isNaN(a)||null==a})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=OC(i,this),n.collapsed=!i.isExpand,n},e.type="series.tree",e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(sg);function NC(t,e){for(var n,i=[t];n=i.pop();)if(e(n),n.isExpand){var r=n.children;if(r.length)for(var o=r.length-1;o>=0;o--)i.push(r[o])}}function EC(t,e){t.eachSeriesByType("tree",(function(t){!function(t,e){var n=function(t,e){return xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=n;var i=t.get("layout"),r=0,o=0,a=null;"radial"===i?(r=2*Math.PI,o=Math.min(n.height,n.width)/2,a=iC((function(t,e){return(t.parentNode===e.parentNode?1:2)/t.depth}))):(r=n.width,o=n.height,a=iC());var s=t.getData().tree.root,l=s.children[0];if(l){!function(t){var e=t;e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var n,i,r=[e];n=r.pop();)if(i=n.children,n.isExpand&&i.length)for(var o=i.length-1;o>=0;o--){var a=i[o];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},r.push(a)}}(s),function(t,e,n){for(var i,r=[t],o=[];i=r.pop();)if(o.push(i),i.isExpand){var a=i.children;if(a.length)for(var s=0;sh.getLayout().x&&(h=t),t.depth>c.depth&&(c=t)}));var p=u===h?1:a(u,h)/2,d=p-u.getLayout().x,f=0,g=0,y=0,v=0;if("radial"===i)f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),NC(l,(function(t){y=(t.getLayout().x+d)*f,v=(t.depth-1)*g;var e=rC(y,v);t.setLayout({x:e.x,y:e.y,rawX:y,rawY:v},!0)}));else{var m=t.getOrient();"RL"===m||"LR"===m?(g=o/(h.getLayout().x+p+d),f=r/(c.depth-1||1),NC(l,(function(t){v=(t.getLayout().x+d)*g,y="LR"===m?(t.depth-1)*f:r-(t.depth-1)*f,t.setLayout({x:y,y:v},!0)}))):"TB"!==m&&"BT"!==m||(f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),NC(l,(function(t){y=(t.getLayout().x+d)*f,v="TB"===m?(t.depth-1)*g:o-(t.depth-1)*g,t.setLayout({x:y,y:v},!0)})))}}}(t,e)}))}function zC(t){t.eachSeriesByType("tree",(function(t){var e=t.getData();e.tree.eachNode((function(t){var n=t.getModel().getModel("itemStyle").getItemStyle();A(e.ensureUniqueItemVisual(t.dataIndex,"style"),n)}))}))}var VC=["treemapZoomToNode","treemapRender","treemapMove"];function BC(t){var e=t.getData().tree,n={};e.eachNode((function(e){for(var i=e;i&&i.depth>1;)i=i.parentNode;var r=ed(t.ecModel,i.name||i.dataIndex+"",n);e.setVisual("decal",r)}))}var FC=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventUsingHoverLayer=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};GC(n);var i=t.levels||[],r=this.designatedVisualItemStyle={},o=new dc({itemStyle:r},this,e),a=z((i=t.levels=function(t,e){var n,i,r=ho(e.get("color")),o=ho(e.get(["aria","decal","decals"]));if(!r)return;E(t=t||[],(function(t){var e=new dc(t),r=e.get("color"),o=e.get("decal");(e.get(["itemStyle","color"])||r&&"none"!==r)&&(n=!0),(e.get(["itemStyle","decal"])||o&&"none"!==o)&&(i=!0)}));var a=t[0]||(t[0]={});n||(a.color=r.slice());!i&&o&&(a.decal=o.slice());return t}(i,e))||[],(function(t){return new dc(t,o,e)}),this),s=AC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=s.getNodeByDataIndex(e),i=n?a[n.depth]:null;return t.parentModel=i||o,t}))}));return s.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t);return Xf("nameValue",{name:i.getName(t),value:r})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=OC(i,this),n.treePathInfo=n.treeAncestors,n},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},A(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var e=this._idIndexMap;e||(e=this._idIndexMap=ft(),this._idIndexMapCount=0);var n=e.get(t);return null==n&&e.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){BC(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(sg);function GC(t){var e=0;E(t.children,(function(t){GC(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var WC=function(){function t(t){this.group=new Cr,t.add(this.group)}return t.prototype.render=function(t,e,n,i){var r=t.getModel("breadcrumb"),o=this.group;if(o.removeAll(),r.get("show")&&n){var a=r.getModel("itemStyle"),s=a.getModel("textStyle"),l={pos:{left:r.get("left"),right:r.get("right"),top:r.get("top"),bottom:r.get("bottom")},box:{width:e.getWidth(),height:e.getHeight()},emptyItemWidth:r.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,l,s),this._renderContent(t,l,a,s,i),_p(o,l.pos,l.box)}},t.prototype._prepare=function(t,e,n){for(var i=t;i;i=i.parentNode){var r=xo(i.getModel().get("name"),""),o=n.getTextRect(r),a=Math.max(o.width+16,e.emptyItemWidth);e.totalWidth+=a+8,e.renderList.push({node:i,text:r,width:a})}},t.prototype._renderContent=function(t,e,n,i,r){for(var o,a,s,l,u,h,c,p,d,f=0,g=e.emptyItemWidth,y=t.get(["breadcrumb","height"]),v=(o=e.pos,a=e.box,l=a.width,u=a.height,h=Er(o.left,l),c=Er(o.top,u),p=Er(o.right,l),d=Er(o.bottom,u),(isNaN(h)||isNaN(parseFloat(o.left)))&&(h=0),(isNaN(p)||isNaN(parseFloat(o.right)))&&(p=l),(isNaN(c)||isNaN(parseFloat(o.top)))&&(c=0),(isNaN(d)||isNaN(parseFloat(o.bottom)))&&(d=u),s=ip(s||0),{width:Math.max(p-h-s[1]-s[3],0),height:Math.max(d-c-s[0]-s[2],0)}),m=e.totalWidth,x=e.renderList,_=x.length-1;_>=0;_--){var b=x[_],w=b.node,S=b.width,M=b.text;m>v.width&&(m-=S-g,S=g,M=null);var I=new Pu({shape:{points:HC(f,0,S,y,_===x.length-1,0===_)},style:k(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new ks({style:{text:M,fill:i.getTextColor(),font:i.getFont()}}),textConfig:{position:"inside"},z2:1e5,onclick:H(r,w)});I.disableLabelAnimation=!0,this.group.add(I),YC(I,t,w),f+=S+8}},t.prototype.remove=function(){this.group.removeAll()},t}();function HC(t,e,n,i,r,o){var a=[[r?t:t-5,e],[t+n,e],[t+n,e+i],[r?t:t-5,e+i]];return!o&&a.splice(2,0,[t+n+5,e+i/2]),!r&&a.push([t,e+i/2]),a}function YC(t,e,n){Hs(t).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&OC(n,e)}}var UC=function(){function t(){this._storage=[],this._elExistsMap={}}return t.prototype.add=function(t,e,n,i,r){return!this._elExistsMap[t.id]&&(this._elExistsMap[t.id]=!0,this._storage.push({el:t,target:e,duration:n,delay:i,easing:r}),!0)},t.prototype.finished=function(t){return this._finishedCallback=t,this},t.prototype.start=function(){for(var t=this,e=this._storage.length,n=function(){--e<=0&&(t._storage.length=0,t._elExistsMap={},t._finishedCallback&&t._finishedCallback())},i=0,r=this._storage.length;i3||Math.abs(t.dy)>3)){var e=this.seriesModel.getData().tree.root;if(!e)return;var n=e.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var e=t.originX,n=t.originY;if("animating"!==this._state){var i=this.seriesModel.getData().tree.root;if(!i)return;var r=i.getLayout();if(!r)return;var o=new sr(r.x,r.y,r.width,r.height),a=this.seriesModel.layoutInfo,s=[1,0,0,1,0,0];Ei(s,s,[-(e-=a.x),-(n-=a.y)]),Vi(s,s,[t.scale,t.scale]),Ei(s,s,[e,n]),o.applyTransform(s),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:o.x,y:o.y,width:o.width,height:o.height}})}},e.prototype._initEvents=function(t){var e=this;t.on("click",(function(t){if("ready"===e._state){var n=e.seriesModel.get("nodeClick",!0);if(n){var i=e.findTarget(t.offsetX,t.offsetY);if(i){var r=i.node;if(r.getLayout().isLeafRoot)e._rootToNode(i);else if("zoomToNode"===n)e._zoomToNode(i);else if("link"===n){var o=r.hostTree.data.getItemModel(r.dataIndex),a=o.get("link",!0),s=o.get("target",!0)||"blank";a&&dp(a,s)}}}}}),this)},e.prototype._renderBreadcrumb=function(t,e,n){var i=this;n||(n=null!=t.get("leafDepth",!0)?{node:t.getViewRoot()}:this.findTarget(e.getWidth()/2,e.getHeight()/2))||(n={node:t.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new WC(this.group))).render(t,e,n.node,(function(e){"animating"!==i._state&&(PC(t.getViewRoot(),e)?i._rootToNode({node:e}):i._zoomToNode({node:e}))}))},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,e){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var r=this._storage.background[i.getRawIndex()];if(r){var o=r.transformCoordToLocal(t,e),a=r.shape;if(!(a.x<=o[0]&&o[0]<=a.x+a.width&&a.y<=o[1]&&o[1]<=a.y+a.height))return!1;n={node:i,offsetX:o[0],offsetY:o[1]}}}),this),n},e.type="treemap",e}(xg);var tD=E,eD=q,nD=-1,iD=function(){function t(e){var n=e.mappingMethod,i=e.type,r=this.option=T(e);this.type=i,this.mappingMethod=n,this._normalizeData=dD[n];var o=t.visualHandlers[i];this.applyVisual=o.applyVisual,this.getColorMapper=o.getColorMapper,this._normalizedToVisual=o._normalizedToVisual[n],"piecewise"===n?(rD(r),function(t){var e=t.pieceList;t.hasSpecialVisual=!1,E(e,(function(e,n){e.originIndex=n,null!=e.visual&&(t.hasSpecialVisual=!0)}))}(r)):"category"===n?r.categories?function(t){var e=t.categories,n=t.categoryMap={},i=t.visual;if(tD(e,(function(t,e){n[t]=e})),!Y(i)){var r=[];q(i)?tD(i,(function(t,e){var i=n[e];r[null!=i?i:nD]=t})):r[-1]=i,i=pD(t,r)}for(var o=e.length-1;o>=0;o--)null==i[o]&&(delete n[e[o]],e.pop())}(r):rD(r,!0):(lt("linear"!==n||r.dataExtent),rD(r))}return t.prototype.mapValueToVisual=function(t){var e=this._normalizeData(t);return this._normalizedToVisual(e,t)},t.prototype.getNormalizer=function(){return W(this._normalizeData,this)},t.listVisualTypes=function(){return G(t.visualHandlers)},t.isValidType=function(e){return t.visualHandlers.hasOwnProperty(e)},t.eachVisual=function(t,e,n){q(t)?E(t,e,n):e.call(n,t)},t.mapVisual=function(e,n,i){var r,o=Y(e)?[]:q(e)?{}:(r=!0,null);return t.eachVisual(e,(function(t,e){var a=n.call(i,t,e);r?o=a:o[e]=a})),o},t.retrieveVisuals=function(e){var n,i={};return e&&tD(t.visualHandlers,(function(t,r){e.hasOwnProperty(r)&&(i[r]=e[r],n=!0)})),n?i:null},t.prepareVisualTypes=function(t){if(Y(t))t=t.slice();else{if(!eD(t))return[];var e=[];tD(t,(function(t,n){e.push(n)})),t=e}return t.sort((function(t,e){return"color"===e&&"color"!==t&&0===t.indexOf("color")?1:-1})),t},t.dependsOn=function(t,e){return"color"===e?!(!t||0!==t.indexOf(e)):t===e},t.findPieceIndex=function(t,e,n){for(var i,r=1/0,o=0,a=e.length;ou[1]&&(u[1]=l);var h=e.get("colorMappingBy"),c={type:a.name,dataExtent:u,visual:a.range};"color"!==c.type||"index"!==h&&"id"!==h?c.mappingMethod="linear":(c.mappingMethod="category",c.loop=!0);var p=new iD(c);return gD(p).drColorMappingBy=h,p}(0,r,o,0,u,d);E(d,(function(t,e){if(t.depth>=n.length||t===n[t.depth]){var o=function(t,e,n,i,r,o){var a=A({},e);if(r){var s=r.type,l="color"===s&&gD(r).drColorMappingBy,u="index"===l?i:"id"===l?o.mapIdToIndex(n.getId()):n.getValue(t.get("visualDimension"));a[s]=r.mapValueToVisual(u)}return a}(r,u,t,e,f,i);vD(t,o,n,i)}}))}else s=mD(u),h.fill=s}}function mD(t){var e=xD(t,"color");if(e){var n=xD(t,"colorAlpha"),i=xD(t,"colorSaturation");return i&&(e=Dn(e,null,null,i)),n&&(e=An(e,n)),e}}function xD(t,e){var n=t[e];if(null!=n&&"none"!==n)return n}function _D(t,e){var n=t.get(e);return Y(n)&&n.length?{name:e,range:n}:null}var bD=Math.max,wD=Math.min,SD=it,MD=E,ID=["itemStyle","borderWidth"],TD=["itemStyle","gapWidth"],CD=["upperLabel","show"],DD=["upperLabel","height"],AD={seriesType:"treemap",reset:function(t,e,n,i){var r=n.getWidth(),o=n.getHeight(),a=t.option,s=xp(t.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()}),l=a.size||[],u=Er(SD(s.width,l[0]),r),h=Er(SD(s.height,l[1]),o),c=i&&i.type,p=kC(i,["treemapZoomToNode","treemapRootToNode"],t),d="treemapRender"===c||"treemapMove"===c?i.rootRect:null,f=t.getViewRoot(),g=LC(f);if("treemapMove"!==c){var y="treemapZoomToNode"===c?function(t,e,n,i,r){var o,a=(e||{}).node,s=[i,r];if(!a||a===n)return s;var l=i*r,u=l*t.option.zoomToNodeRatio;for(;o=a.parentNode;){for(var h=0,c=o.children,p=0,d=c.length;pYr&&(u=Yr),a=o}ua[1]&&(a[1]=e)}))):a=[NaN,NaN];return{sum:i,dataExtent:a}}(e,a,s);if(0===u.sum)return t.viewChildren=[];if(u.sum=function(t,e,n,i,r){if(!i)return n;for(var o=t.get("visibleMin"),a=r.length,s=a,l=a-1;l>=0;l--){var u=r["asc"===i?a-l-1:l].getValue();u/n*ei&&(i=a));var l=t.area*t.area,u=e*e*n;return l?bD(u*i/l,l/(u*r)):1/0}function PD(t,e,n,i,r){var o=e===n.width?0:1,a=1-o,s=["x","y"],l=["width","height"],u=n[s[o]],h=e?t.area/e:0;(r||h>n[l[a]])&&(h=n[l[a]]);for(var c=0,p=t.length;ci&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a0&&(m[0]=-m[0],m[1]=-m[1]);var _=v[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var b=-Math.atan2(v[1],v[0]);u[0].8?"left":h[0]<-.8?"right":"center",p=h[1]>.8?"top":h[1]<-.8?"bottom":"middle";break;case"start":i.x=-h[0]*f+l[0],i.y=-h[1]*g+l[1],c=h[0]>.8?"right":h[0]<-.8?"left":"center",p=h[1]>.8?"bottom":h[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=f*_+l[0],i.y=l[1]+w,c=v[0]<0?"right":"left",i.originX=-f*_,i.originY=-w;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+w,c="center",i.originY=-w;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-f*_+u[0],i.y=u[1]+w,c=v[0]>=0?"right":"left",i.originX=f*_,i.originY=-w}i.scaleX=i.scaleY=r,i.setStyle({verticalAlign:i.__verticalAlign||p,align:i.__align||c})}}}function S(t,e){var n=t.__specifiedRotation;if(null==n){var i=a.tangentAt(e);t.attr("rotation",(1===e?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else t.attr("rotation",n)}},e}(Cr),gA=function(){function t(t){this.group=new Cr,this._LineCtor=t||fA}return t.prototype.updateData=function(t){var e=this;this._progressiveEls=null;var n=this,i=n.group,r=n._lineData;n._lineData=t,r||i.removeAll();var o=yA(t);t.diff(r).add((function(n){e._doAdd(t,n,o)})).update((function(n,i){e._doUpdate(r,t,i,n,o)})).remove((function(t){i.remove(r.getItemGraphicEl(t))})).execute()},t.prototype.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl((function(e,n){e.updateLayout(t,n)}),this)},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=yA(t),this._lineData=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e){function n(t){t.isGroup||function(t){return t.animators&&t.animators.length>0}(t)||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=t.start;i=0?i+=u:i-=u:f>=0?i-=u:i+=u}return i}function TA(t,e){var n=[],i=Qe,r=[[],[],[]],o=[[],[]],a=[];e/=2,t.eachEdge((function(t,s){var l=t.getLayout(),u=t.getVisual("fromSymbol"),h=t.getVisual("toSymbol");l.__original||(l.__original=[Mt(l[0]),Mt(l[1])],l[2]&&l.__original.push(Mt(l[2])));var c=l.__original;if(null!=l[2]){if(St(r[0],c[0]),St(r[1],c[2]),St(r[2],c[1]),u&&"none"!==u){var p=KD(t.node1),d=IA(r,c[0],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[0][0]=n[3],r[1][0]=n[4],i(r[0][1],r[1][1],r[2][1],d,n),r[0][1]=n[3],r[1][1]=n[4]}if(h&&"none"!==h){p=KD(t.node2),d=IA(r,c[1],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[1][0]=n[1],r[2][0]=n[2],i(r[0][1],r[1][1],r[2][1],d,n),r[1][1]=n[1],r[2][1]=n[2]}St(l[0],r[0]),St(l[1],r[2]),St(l[2],r[1])}else{if(St(o[0],c[0]),St(o[1],c[1]),Dt(a,o[1],o[0]),Rt(a,a),u&&"none"!==u){p=KD(t.node1);Ct(o[0],o[0],a,p*e)}if(h&&"none"!==h){p=KD(t.node2);Ct(o[1],o[1],a,-p*e)}St(l[0],o[0]),St(l[1],o[1])}}))}function CA(t){return"view"===t.type}var DA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){var n=new qw,i=new gA,r=this.group;this._controller=new kI(e.getZr()),this._controllerHost={target:r},r.add(n.group),r.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},e.prototype.render=function(t,e,n){var i=this,r=t.coordinateSystem;this._model=t;var o=this._symbolDraw,a=this._lineDraw,s=this.group;if(CA(r)){var l={x:r.x,y:r.y,scaleX:r.scaleX,scaleY:r.scaleY};this._firstRender?s.attr(l):rh(s,l,t)}TA(t.getGraph(),qD(t));var u=t.getData();o.updateData(u);var h=t.getEdgeData();a.updateData(h),this._updateNodeAndLinkScale(),this._updateController(t,e,n),clearTimeout(this._layoutTimeout);var c=t.forceLayout,p=t.get(["force","layoutAnimation"]);c&&this._startForceLayoutIteration(c,p),u.graph.eachNode((function(t){var e=t.dataIndex,n=t.getGraphicEl(),r=t.getModel();if(n){n.off("drag").off("dragend");var o=r.get("draggable");o&&n.on("drag",(function(){c&&(c.warmUp(),!i._layouting&&i._startForceLayoutIteration(c,p),c.setFixed(e),u.setItemLayout(e,[n.x,n.y]))})).on("dragend",(function(){c&&c.setUnfixed(e)})),n.setDraggable(o&&!!c,!!r.get("cursor")),"adjacency"===r.get(["emphasis","focus"])&&(Hs(n).focus=t.getAdjacentDataIndices())}})),u.graph.eachEdge((function(t){var e=t.getGraphicEl(),n=t.getModel().get(["emphasis","focus"]);e&&"adjacency"===n&&(Hs(e).focus={edge:[t.dataIndex],node:[t.node1.dataIndex,t.node2.dataIndex]})}));var d="circular"===t.get("layout")&&t.get(["circular","rotateLabel"]),f=u.getLayout("cx"),g=u.getLayout("cy");u.eachItemGraphicEl((function(t,e){var n=u.getItemModel(e).get(["label","rotate"])||0,i=t.getSymbolPath();if(d){var r=u.getItemLayout(e),o=Math.atan2(r[1]-g,r[0]-f);o<0&&(o=2*Math.PI+o);var a=r[0]=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r=0&&n[r].node1.dataIndex>=0&&n[r].node2.dataIndex>=0&&t.call(e,n[r],r)},t.prototype.breadthFirstTraverse=function(t,e,n,i){if(e instanceof LA||(e=this._nodesMap[AA(e)]),e){for(var r="out"===n?"outEdges":"in"===n?"inEdges":"edges",o=0;o=0&&n.node2.dataIndex>=0}));for(r=0,o=i.length;r=0&&this[t][e].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[t][e].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[t][e].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[t][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[t][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[t][e].getRawIndex(this.dataIndex)}}}function RA(t,e,n,i,r){for(var o=new kA(i),a=0;a "+p)),u++)}var d,f=n.get("coordinateSystem");if("cartesian2d"===f||"polar"===f)d=rx(t,n);else{var g=hd.get(f),y=g&&g.dimensions||[];P(y,"value")<0&&y.concat(["value"]);var v=Km(t,{coordDimensions:y,encodeDefine:n.getEncode()}).dimensions;(d=new qm(v,n)).initData(t)}var m=new qm(["value"],n);return m.initData(l,s),r&&r(d,m),_C({mainData:d,struct:o,structAttr:"graph",datas:{node:d,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),o.update(),o}R(LA,OA("hostGraph","data")),R(PA,OA("hostGraph","edgeData"));var NA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new hM(i,i),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeDefaultAndTheme=function(e){t.prototype.mergeDefaultAndTheme.apply(this,arguments),co(e,"edgeLabel",["show"])},e.prototype.getInitialData=function(t,e){var n,i=t.edges||t.links||[],r=t.data||t.nodes||[],o=this;if(r&&i){FD(n=this)&&(n.__curvenessList=[],n.__edgeMap={},GD(n));var a=RA(r,i,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t){var e=o._categoriesModels[t.getShallow("category")];return e&&(e.parentModel=t.parentModel,t.parentModel=e),t}));var n=dc.prototype.getModel;function i(t,e){var i=n.call(this,t,e);return i.resolveParentPath=r,i}function r(t){if(t&&("label"===t[0]||"label"===t[1])){var e=t.slice();return"label"===t[0]?e[0]="edgeLabel":"label"===t[1]&&(e[1]="edgeLabel"),e}return t}e.wrapMethod("getItemModel",(function(t){return t.resolveParentPath=r,t.getModel=i,t}))}));return E(a.edges,(function(t){!function(t,e,n,i){if(FD(n)){var r=WD(t,e,n),o=n.__edgeMap,a=o[HD(r)];o[r]&&!a?o[r].isForward=!0:a&&o[r]&&(a.isForward=!0,o[r].isForward=!1),o[r]=o[r]||[],o[r].push(i)}}(t.node1,t.node2,this,t.dataIndex)}),this),a.data}},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.getCategoriesData=function(){return this._categoriesData},e.prototype.formatTooltip=function(t,e,n){if("edge"===n){var i=this.getData(),r=this.getDataParams(t,n),o=i.graph.getEdgeByIndex(t),a=i.getName(o.node1.dataIndex),s=i.getName(o.node2.dataIndex),l=[];return null!=a&&l.push(a),null!=s&&l.push(s),Xf("nameValue",{name:l.join(" > "),value:r.value,noValue:null==r.value})}return rg({series:this,dataIndex:t,multipleSeries:e})},e.prototype._updateCategoriesData=function(){var t=z(this.option.categories||[],(function(t){return null!=t.value?t:A({value:0},t)})),e=new qm(["value"],this);e.initData(t),this._categoriesData=e,this._categoriesModels=e.mapArray((function(t){return e.getItemModel(t)}))},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.isAnimationEnabled=function(){return t.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},e.type="series.graph",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(sg),EA={type:"graphRoam",event:"graphRoam",update:"none"};var zA=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},VA=function(t){function e(e){var n=t.call(this,e)||this;return n.type="pointer",n}return n(e,t),e.prototype.getDefaultShape=function(){return new zA},e.prototype.buildPath=function(t,e){var n=Math.cos,i=Math.sin,r=e.r,o=e.width,a=e.angle,s=e.x-n(a)*o*(o>=r/3?1:2),l=e.y-i(a)*o*(o>=r/3?1:2);a=e.angle-Math.PI/2,t.moveTo(s,l),t.lineTo(e.x+n(a)*o,e.y+i(a)*o),t.lineTo(e.x+n(e.angle)*r,e.y+i(e.angle)*r),t.lineTo(e.x-n(a)*o,e.y-i(a)*o),t.lineTo(s,l)},e}(gs);function BA(t,e){var n=null==t?"":t+"";return e&&(X(e)?n=e.replace("{value}",n):U(e)&&(n=e(t))),n}var FA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),r=function(t,e){var n=t.get("center"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r);return{cx:Er(n[0],e.getWidth()),cy:Er(n[1],e.getHeight()),r:Er(t.get("radius"),o/2)}}(t,n);this._renderMain(t,e,n,i,r),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,e,n,i,r){var o=this.group,a=t.get("clockwise"),s=-t.get("startAngle")/180*Math.PI,l=-t.get("endAngle")/180*Math.PI,u=t.getModel("axisLine"),h=u.get("roundCap")?DS:Cu,c=u.get("show"),p=u.getModel("lineStyle"),d=p.get("width"),f=[s,l];ja(f,!a);for(var g=(l=f[1])-(s=f[0]),y=s,v=0;c&&v=t&&(0===e?0:i[e-1][0]).8?"bottom":"middle",align:u<-.4?"left":u>.4?"right":"center"},{inheritColor:R}),silent:!0}))}if(m.get("show")&&k!==_){P=(P=m.get("distance"))?P+l:l;for(var N=0;N<=b;N++){u=Math.cos(M),h=Math.sin(M);var E=new zu({shape:{x1:u*(f-P)+p,y1:h*(f-P)+d,x2:u*(f-S-P)+p,y2:h*(f-S-P)+d},silent:!0,style:D});"auto"===D.stroke&&E.setStyle({stroke:i((k+N/b)/_)}),c.add(E),M+=T}M-=T}else M+=I}},e.prototype._renderPointer=function(t,e,n,i,r,o,a,s,l){var u=this.group,h=this._data,c=this._progressEls,p=[],d=t.get(["pointer","show"]),f=t.getModel("progress"),g=f.get("show"),y=t.getData(),v=y.mapDimension("value"),m=+t.get("min"),x=+t.get("max"),_=[m,x],b=[o,a];function w(e,n){var i,o=y.getItemModel(e).getModel("pointer"),a=Er(o.get("width"),r.r),s=Er(o.get("length"),r.r),l=t.get(["pointer","icon"]),u=o.get("offsetCenter"),h=Er(u[0],r.r),c=Er(u[1],r.r),p=o.get("keepAspect");return(i=l?Ly(l,h-a/2,c-s,a,s,null,p):new VA({shape:{angle:-Math.PI/2,width:a,r:s,x:h,y:c}})).rotation=-(n+Math.PI/2),i.x=r.cx,i.y=r.cy,i}function S(t,e){var n=f.get("roundCap")?DS:Cu,i=f.get("overlap"),a=i?f.get("width"):l/y.count(),u=i?r.r-a:r.r-(t+1)*a,h=i?r.r:r.r-t*a,c=new n({shape:{startAngle:o,endAngle:e,cx:r.cx,cy:r.cy,clockwise:s,r0:u,r:h}});return i&&(c.z2=x-y.get(v,t)%x),c}(g||d)&&(y.diff(h).add((function(e){var n=y.get(v,e);if(d){var i=w(e,o);oh(i,{rotation:-((isNaN(+n)?b[0]:Nr(n,_,b,!0))+Math.PI/2)},t),u.add(i),y.setItemGraphicEl(e,i)}if(g){var r=S(e,o),a=f.get("clip");oh(r,{shape:{endAngle:Nr(n,_,b,a)}},t),u.add(r),Ys(t.seriesIndex,y.dataType,e,r),p[e]=r}})).update((function(e,n){var i=y.get(v,e);if(d){var r=h.getItemGraphicEl(n),a=r?r.rotation:o,s=w(e,a);s.rotation=a,rh(s,{rotation:-((isNaN(+i)?b[0]:Nr(i,_,b,!0))+Math.PI/2)},t),u.add(s),y.setItemGraphicEl(e,s)}if(g){var l=c[n],m=S(e,l?l.shape.endAngle:o),x=f.get("clip");rh(m,{shape:{endAngle:Nr(i,_,b,x)}},t),u.add(m),Ys(t.seriesIndex,y.dataType,e,m),p[e]=m}})).execute(),y.each((function(t){var e=y.getItemModel(t),n=e.getModel("emphasis"),r=n.get("focus"),o=n.get("blurScope"),a=n.get("disabled");if(d){var s=y.getItemGraphicEl(t),l=y.getItemVisual(t,"style"),u=l.fill;if(s instanceof _s){var h=s.style;s.useStyle(A({image:h.image,x:h.x,y:h.y,width:h.width,height:h.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(u);s.setStyle(e.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Nr(y.get(v,t),_,[0,1],!0))),s.z2EmphasisLift=0,Vl(s,e),Rl(s,r,o,a)}if(g){var c=p[t];c.useStyle(y.getItemVisual(t,"style")),c.setStyle(e.getModel(["progress","itemStyle"]).getItemStyle()),c.z2EmphasisLift=0,Vl(c,e),Rl(c,r,o,a)}})),this._progressEls=p)},e.prototype._renderAnchor=function(t,e){var n=t.getModel("anchor");if(n.get("show")){var i=n.get("size"),r=n.get("icon"),o=n.get("offsetCenter"),a=n.get("keepAspect"),s=Ly(r,e.cx-i/2+Er(o[0],e.r),e.cy-i/2+Er(o[1],e.r),i,i,null,a);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},e.prototype._renderTitleAndDetail=function(t,e,n,i,r){var o=this,a=t.getData(),s=a.mapDimension("value"),l=+t.get("min"),u=+t.get("max"),h=new Cr,c=[],p=[],d=t.isAnimationEnabled(),f=t.get(["pointer","showAbove"]);a.diff(this._data).add((function(t){c[t]=new ks({silent:!0}),p[t]=new ks({silent:!0})})).update((function(t,e){c[t]=o._titleEls[e],p[t]=o._detailEls[e]})).execute(),a.each((function(e){var n=a.getItemModel(e),o=a.get(s,e),g=new Cr,y=i(Nr(o,[l,u],[0,1],!0)),v=n.getModel("title");if(v.get("show")){var m=v.get("offsetCenter"),x=r.cx+Er(m[0],r.r),_=r.cy+Er(m[1],r.r);(D=c[e]).attr({z2:f?0:2,style:Uh(v,{x:x,y:_,text:a.getName(e),align:"center",verticalAlign:"middle"},{inheritColor:y})}),g.add(D)}var b=n.getModel("detail");if(b.get("show")){var w=b.get("offsetCenter"),S=r.cx+Er(w[0],r.r),M=r.cy+Er(w[1],r.r),I=Er(b.get("width"),r.r),T=Er(b.get("height"),r.r),C=t.get(["progress","show"])?a.getItemVisual(e,"style").fill:y,D=p[e],A=b.get("formatter");D.attr({z2:f?0:2,style:Uh(b,{x:S,y:M,text:BA(o,A),width:isNaN(I)?null:I,height:isNaN(T)?null:T,align:"center",verticalAlign:"middle"},{inheritColor:C})}),Qh(D,{normal:b},o,(function(t){return BA(t,A)})),d&&tc(D,e,a,t,{getFormattedLabel:function(t,e,n,i,r,a){return BA(a?a.interpolatedValue:o,A)}}),g.add(D)}h.add(g)})),this.group.add(h),this._titleEls=c,this._detailEls=p},e.type="gauge",e}(xg),GA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="itemStyle",n}return n(e,t),e.prototype.getInitialData=function(t,e){return uM(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(sg);var WA=["itemStyle","opacity"],HA=function(t){function e(e,n){var i=t.call(this)||this,r=i,o=new Ru,a=new ks;return r.setTextContent(a),i.setTextGuideLine(o),i.updateData(e,n,!0),i}return n(e,t),e.prototype.updateData=function(t,e,n){var i=this,r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e),s=o.getModel("emphasis"),l=o.get(WA);l=null==l?1:l,n||hh(i),i.useStyle(t.getItemVisual(e,"style")),i.style.lineJoin="round",n?(i.setShape({points:a.points}),i.style.opacity=0,oh(i,{style:{opacity:l}},r,e)):rh(i,{style:{opacity:l},shape:{points:a.points}},r,e),Vl(i,o),this._updateLabel(t,e),Rl(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},e.prototype._updateLabel=function(t,e){var n=this,i=this.getTextGuideLine(),r=n.getTextContent(),o=t.hostModel,a=t.getItemModel(e),s=t.getItemLayout(e).label,l=t.getItemVisual(e,"style"),u=l.fill;Hh(r,Yh(a),{labelFetcher:t.hostModel,labelDataIndex:e,defaultOpacity:l.opacity,defaultText:t.getName(e)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:u,outsideFill:u});var h=s.linePoints;i.setShape({points:h}),n.textGuideLineConfig={anchor:h?new Ji(h[0][0],h[0][1]):null},rh(r,{style:{x:s.x,y:s.y}},o,e),r.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),db(n,fb(a),{stroke:u})},e}(Pu),YA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreLabelLineUpdate=!0,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group;i.diff(r).add((function(t){var e=new HA(i,t);i.setItemGraphicEl(t,e),o.add(e)})).update((function(t,e){var n=r.getItemGraphicEl(e);n.updateData(i,t),o.add(n),i.setItemGraphicEl(t,n)})).remove((function(e){uh(r.getItemGraphicEl(e),t,e)})).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type="funnel",e}(xg),UA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new hM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.getInitialData=function(t,e){return uM(this,{coordDimensions:["value"],encodeDefaulter:H(Yp,this)})},e.prototype._defaultLabelLine=function(t){co(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=n.mapDimension("value"),o=n.getSum(r);return i.percent=o?+(n.get(r,e)/o*100).toFixed(2):0,i.$vars.push("percent"),i},e.type="series.funnel",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(sg);function XA(t,e){t.eachSeriesByType("funnel",(function(t){var n=t.getData(),i=n.mapDimension("value"),r=t.get("sort"),o=function(t,e){return xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e),a=t.get("orient"),s=o.width,l=o.height,u=function(t,e){for(var n=t.mapDimension("value"),i=t.mapArray(n,(function(t){return t})),r=[],o="ascending"===e,a=0,s=t.count();a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&ok(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function ok(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var ak=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(Tp),sk=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(H_);function lk(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=hk(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=hk(s,[0,a]),r=o=hk(s,[r,o]),i=0}e[0]=hk(e[0],n),e[1]=hk(e[1],n);var l=uk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=hk(e[i],c),u=uk(e,i),null!=r&&(u.sign!==l.sign||u.spano&&(e[1-i]=e[i]+u.sign*o),e}function uk(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function hk(t,e){return Math.min(null!=e[1]?e[1]:1/0,Math.max(null!=e[0]?e[0]:-1/0,t))}var ck=E,pk=Math.min,dk=Math.max,fk=Math.floor,gk=Math.ceil,yk=zr,vk=Math.PI,mk=function(){function t(t,e,n){this.type="parallel",this._axesMap=ft(),this._axesLayout={},this.dimensions=t.dimensions,this._model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=t.dimensions,r=t.parallelAxisIndex;ck(i,(function(t,n){var i=r[n],o=e.getComponent("parallelAxis",i),a=this._axesMap.set(t,new sk(t,o_(o),[0,0],o.get("type"),i)),s="category"===a.type;a.onBand=s&&o.get("boundaryGap"),a.inverse=o.get("inverse"),o.axis=a,a.model=o,a.coordinateSystem=o.coordinateSystem=this}),this)},t.prototype.update=function(t,e){this._updateAxesFromSeries(this._model,t)},t.prototype.containPoint=function(t){var e=this._makeLayoutInfo(),n=e.axisBase,i=e.layoutBase,r=e.pixelDimIndex,o=t[1-r],a=t[r];return o>=n&&o<=n+e.axisLength&&a>=i&&a<=i+e.layoutLength},t.prototype.getModel=function(){return this._model},t.prototype._updateAxesFromSeries=function(t,e){e.eachSeries((function(n){if(t.contains(n,e)){var i=n.getData();ck(this.dimensions,(function(t){var e=this._axesMap.get(t);e.scale.unionExtentFromData(i,i.mapDimension(t)),r_(e.scale,e.model)}),this)}}),this)},t.prototype.resize=function(t,e){this._rect=xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()}),this._layoutAxes()},t.prototype.getRect=function(){return this._rect},t.prototype._makeLayoutInfo=function(){var t,e=this._model,n=this._rect,i=["x","y"],r=["width","height"],o=e.get("layout"),a="horizontal"===o?0:1,s=n[r[a]],l=[0,s],u=this.dimensions.length,h=xk(e.get("axisExpandWidth"),l),c=xk(e.get("axisExpandCount")||0,[0,u]),p=e.get("axisExpandable")&&u>3&&u>c&&c>1&&h>0&&s>0,d=e.get("axisExpandWindow");d?(t=xk(d[1]-d[0],l),d[1]=d[0]+t):(t=xk(h*(c-1),l),(d=[h*(e.get("axisExpandCenter")||fk(u/2))-t/2])[1]=d[0]+t);var f=(s-t)/(u-c);f<3&&(f=0);var g=[fk(yk(d[0]/h,1))+1,gk(yk(d[1]/h,1))-1],y=f/h*d[0];return{layout:o,pixelDimIndex:a,layoutBase:n[i[a]],layoutLength:s,axisBase:n[i[1-a]],axisLength:n[r[1-a]],axisExpandable:p,axisExpandWidth:h,axisCollapseWidth:f,axisExpandWindow:d,axisCount:u,winInnerIndices:g,axisExpandWindow0Pos:y}},t.prototype._layoutAxes=function(){var t=this._rect,e=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),r=i.layout;e.each((function(t){var e=[0,i.axisLength],n=t.inverse?1:0;t.setExtent(e[n],e[1-n])})),ck(n,(function(e,n){var o=(i.axisExpandable?bk:_k)(n,i),a={horizontal:{x:o.position,y:i.axisLength},vertical:{x:0,y:o.position}},s={horizontal:vk/2,vertical:0},l=[a[r].x+t.x,a[r].y+t.y],u=s[r],h=[1,0,0,1,0,0];zi(h,h,u),Ei(h,h,l),this._axesLayout[e]={position:l,rotation:u,transform:h,axisNameAvailableWidth:o.axisNameAvailableWidth,axisLabelShow:o.axisLabelShow,nameTruncateMaxWidth:o.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},t.prototype.getAxis=function(t){return this._axesMap.get(t)},t.prototype.dataToPoint=function(t,e){return this.axisCoordToPoint(this._axesMap.get(e).dataToCoord(t),e)},t.prototype.eachActiveState=function(t,e,n,i){null==n&&(n=0),null==i&&(i=t.count());var r=this._axesMap,o=this.dimensions,a=[],s=[];E(o,(function(e){a.push(t.mapDimension(e)),s.push(r.get(e).model)}));for(var l=this.hasAxisBrushed(),u=n;ur*(1-h[0])?(l="jump",a=s-r*(1-h[2])):(a=s-r*h[1])>=0&&(a=s-r*(1-h[1]))<=0&&(a=0),(a*=e.axisExpandWidth/u)?lk(a,i,o,"all"):l="none";else{var p=i[1]-i[0];(i=[dk(0,o[1]*s/p-p/2)])[1]=pk(o[1],i[0]+p),i[0]=i[1]-p}return{axisExpandWindow:i,behavior:l}},t}();function xk(t,e){return pk(dk(t,e[0]),e[1])}function _k(t,e){var n=e.layoutLength/(e.axisCount-1);return{position:n*t,axisNameAvailableWidth:n,axisLabelShow:!0}}function bk(t,e){var n,i,r=e.layoutLength,o=e.axisExpandWidth,a=e.axisCount,s=e.axisCollapseWidth,l=e.winInnerIndices,u=s,h=!1;return t=0;n--)Vr(e[n])},e.prototype.getActiveState=function(t){var e=this.activeIntervals;if(!e.length)return"normal";if(null==t||isNaN(+t))return"inactive";if(1===e.length){var n=e[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,r=e.length;i6}(t)||o){if(a&&!o){"single"===s.brushMode&&Wk(t);var l=T(s);l.brushType=oL(l.brushType,a),l.panelId=a===Mk?null:a.panelId,o=t._creatingCover=Rk(t,l),t._covers.push(o)}if(o){var u=lL[oL(t._brushType,a)];o.__brushOption.range=u.getCreatingRange(eL(t,o,t._track)),i&&(Nk(t,o),u.updateCommon(t,o)),Ek(t,o),r={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&Fk(t,e,n)&&Wk(t)&&(r={isEnd:i,removeOnClick:!0});return r}function oL(t,e){return"auto"===t?e.defaultBrushType:t}var aL={mousedown:function(t){if(this._dragging)sL(this,t);else if(!t.target||!t.target.draggable){nL(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=Fk(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=t.offsetX,n=t.offsetY,i=this.group.transformCoordToLocal(e,n);if(function(t,e,n){if(t._brushType&&!function(t,e,n){var i=t._zr;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}(t,e.offsetX,e.offsetY)){var i=t._zr,r=t._covers,o=Fk(t,e,n);if(!t._dragging)for(var a=0;a=0&&(o[r[a].depth]=new dc(r[a],this,e));if(i&&n)return RA(i,n,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getData().getItemLayout(e);if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t})),e.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getGraph().getEdgeByIndex(e).node1.getLayout();if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t}))})).data},e.prototype.setNodePosition=function(t,e){var n=(this.option.data||this.option.nodes)[t];n.localX=e[0],n.localY=e[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,e,n){function i(t){return isNaN(t)||null==t}if("edge"===n){var r=this.getDataParams(t,n),o=r.data,a=r.value;return Xf("nameValue",{name:o.source+" -- "+o.target,value:a,noValue:i(a)})}var s=this.getGraph().getNodeByIndex(t).getLayout().value,l=this.getDataParams(t,n).data.name;return Xf("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if(null==i.value&&"node"===n){var r=this.getGraph().getNodeByIndex(e).getLayout().value;i.value=r}return i},e.type="series.sankey",e.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3},e}(sg);function SL(t,e){t.eachSeriesByType("sankey",(function(t){var n=t.get("nodeWidth"),i=t.get("nodeGap"),r=function(t,e){return xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=r;var o=r.width,a=r.height,s=t.getGraph(),l=s.nodes,u=s.edges;!function(t){E(t,(function(t){var e=OL(t.outEdges,PL),n=OL(t.inEdges,PL),i=t.getValue()||0,r=Math.max(e,n,i);t.setLayout({value:r},!0)}))}(l),function(t,e,n,i,r,o,a,s,l){(function(t,e,n,i,r,o,a){for(var s=[],l=[],u=[],h=[],c=0,p=0;p=0;v&&y.depth>d&&(d=y.depth),g.setLayout({depth:v?y.depth:c},!0),"vertical"===o?g.setLayout({dy:n},!0):g.setLayout({dx:n},!0);for(var m=0;mc-1?d:c-1;a&&"left"!==a&&function(t,e,n,i){if("right"===e){for(var r=[],o=t,a=0;o.length;){for(var s=0;s0;o--)TL(s,l*=.99,a),IL(s,r,n,i,a),RL(s,l,a),IL(s,r,n,i,a)}(t,e,o,r,i,a,s),function(t,e){var n="vertical"===e?"x":"y";E(t,(function(t){t.outEdges.sort((function(t,e){return t.node2.getLayout()[n]-e.node2.getLayout()[n]})),t.inEdges.sort((function(t,e){return t.node1.getLayout()[n]-e.node1.getLayout()[n]}))})),E(t,(function(t){var e=0,n=0;E(t.outEdges,(function(t){t.setLayout({sy:e},!0),e+=t.getLayout().dy})),E(t.inEdges,(function(t){t.setLayout({ty:n},!0),n+=t.getLayout().dy}))}))}(t,s)}(l,u,n,i,o,a,0!==B(l,(function(t){return 0===t.getLayout().value})).length?0:t.get("layoutIterations"),t.get("orient"),t.get("nodeAlign"))}))}function ML(t){var e=t.hostGraph.data.getRawDataItem(t.dataIndex);return null!=e.depth&&e.depth>=0}function IL(t,e,n,i,r){var o="vertical"===r?"x":"y";E(t,(function(t){var a,s,l;t.sort((function(t,e){return t.getLayout()[o]-e.getLayout()[o]}));for(var u=0,h=t.length,c="vertical"===r?"dx":"dy",p=0;p0&&(a=s.getLayout()[o]+l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]+s.getLayout()[c]+e;if((l=u-e-("vertical"===r?i:n))>0){a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0),u=a;for(p=h-2;p>=0;--p)(l=(s=t[p]).getLayout()[o]+s.getLayout()[c]+e-u)>0&&(a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]}}))}function TL(t,e,n){E(t.slice().reverse(),(function(t){E(t,(function(t){if(t.outEdges.length){var i=OL(t.outEdges,CL,n)/OL(t.outEdges,PL);if(isNaN(i)){var r=t.outEdges.length;i=r?OL(t.outEdges,DL,n)/r:0}if("vertical"===n){var o=t.getLayout().x+(i-LL(t,n))*e;t.setLayout({x:o},!0)}else{var a=t.getLayout().y+(i-LL(t,n))*e;t.setLayout({y:a},!0)}}}))}))}function CL(t,e){return LL(t.node2,e)*t.getValue()}function DL(t,e){return LL(t.node2,e)}function AL(t,e){return LL(t.node1,e)*t.getValue()}function kL(t,e){return LL(t.node1,e)}function LL(t,e){return"vertical"===e?t.getLayout().x+t.getLayout().dx/2:t.getLayout().y+t.getLayout().dy/2}function PL(t){return t.getValue()}function OL(t,e,n){for(var i=0,r=t.length,o=-1;++oi&&(i=e)})),E(e,(function(e){var r=new iD({type:"color",mappingMethod:"linear",dataExtent:[n,i],visual:t.get("color")}).mapValueToVisual(e.getLayout().value),o=e.getModel().get(["itemStyle","color"]);null!=o?(e.setVisual("color",o),e.setVisual("style",{fill:o})):(e.setVisual("color",r),e.setVisual("style",{fill:r}))}))}}))}var EL=function(){function t(){}return t.prototype.getInitialData=function(t,e){var n,i,r=e.getComponent("xAxis",this.get("xAxisIndex")),o=e.getComponent("yAxis",this.get("yAxisIndex")),a=r.get("type"),s=o.get("type");"category"===a?(t.layout="horizontal",n=r.getOrdinalMeta(),i=!0):"category"===s?(t.layout="vertical",n=o.getOrdinalMeta(),i=!0):t.layout=t.layout||"horizontal";var l=["x","y"],u="horizontal"===t.layout?0:1,h=this._baseAxisDim=l[u],c=l[1-u],p=[r,o],d=p[u].get("type"),f=p[1-u].get("type"),g=t.data;if(g&&i){var y=[];E(g,(function(t,e){var n;Y(t)?(n=t.slice(),t.unshift(e)):Y(t.value)?((n=A({},t)).value=n.value.slice(),t.value.unshift(e)):n=t,y.push(n)})),t.data=y}var v=this.defaultValueDimensions,m=[{name:h,type:Dm(d),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:c,type:Dm(f),dimsDef:v.slice()}];return uM(this,{coordDimensions:m,dimensionsCount:v.length+1,encodeDefaulter:H(Hp,m,this)})},t.prototype.getBaseAxis=function(){var t=this._baseAxisDim;return this.ecModel.getComponent(t+"Axis",this.get(t+"AxisIndex")).axis},t}(),zL=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return n(e,t),e.type="series.boxplot",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},e}(sg);R(zL,EL,!0);var VL=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this.group,o=this._data;this._data||r.removeAll();var a="horizontal"===t.get("layout")?1:0;i.diff(o).add((function(t){if(i.hasValue(t)){var e=GL(i.getItemLayout(t),i,t,a,!0);i.setItemGraphicEl(t,e),r.add(e)}})).update((function(t,e){var n=o.getItemGraphicEl(e);if(i.hasValue(t)){var s=i.getItemLayout(t);n?(hh(n),WL(s,n,i,t)):n=GL(s,i,t,a),r.add(n),i.setItemGraphicEl(t,n)}else r.remove(n)})).remove((function(t){var e=o.getItemGraphicEl(t);e&&r.remove(e)})).execute(),this._data=i},e.prototype.remove=function(t){var e=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(t){t&&e.remove(t)}))},e.type="boxplot",e}(xg),BL=function(){},FL=function(t){function e(e){var n=t.call(this,e)||this;return n.type="boxplotBoxPath",n}return n(e,t),e.prototype.getDefaultShape=function(){return new BL},e.prototype.buildPath=function(t,e){var n=e.points,i=0;for(t.moveTo(n[i][0],n[i][1]),i++;i<4;i++)t.lineTo(n[i][0],n[i][1]);for(t.closePath();ig){var _=[v,x];i.push(_)}}}return{boxData:n,outliers:i}}(e.getRawData(),t.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:i.boxData},{data:i.outliers}]}};var jL=["color","borderColor"],qL=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,e,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,e,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,e):this._incrementalRenderNormal(t,e)},e.prototype.eachRendered=function(t){Vh(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var e=t.pipelineContext.large;null!=this._isLargeDraw&&e===this._isLargeDraw||(this._isLargeDraw=e,this._clear())},e.prototype._renderNormal=function(t){var e=t.getData(),n=this._data,i=this.group,r=e.getLayout("isSimpleBox"),o=t.get("clip",!0),a=t.coordinateSystem,s=a.getArea&&a.getArea();this._data||i.removeAll(),e.diff(n).add((function(n){if(e.hasValue(n)){var a=e.getItemLayout(n);if(o&&QL(s,a))return;var l=JL(a,n,!0);oh(l,{shape:{points:a.ends}},t,n),tP(l,e,n,r),i.add(l),e.setItemGraphicEl(n,l)}})).update((function(a,l){var u=n.getItemGraphicEl(l);if(e.hasValue(a)){var h=e.getItemLayout(a);o&&QL(s,h)?i.remove(u):(u?(rh(u,{shape:{points:h.ends}},t,a),hh(u)):u=JL(h),tP(u,e,a,r),i.add(u),e.setItemGraphicEl(a,u))}else i.remove(u)})).remove((function(t){var e=n.getItemGraphicEl(t);e&&i.remove(e)})).execute(),this._data=e},e.prototype._renderLarge=function(t){this._clear(),rP(t,this.group);var e=t.get("clip",!0)?lS(t.coordinateSystem,!1,t):null;e?this.group.setClipPath(e):this.group.removeClipPath()},e.prototype._incrementalRenderNormal=function(t,e){for(var n,i=e.getData(),r=i.getLayout("isSimpleBox");null!=(n=t.next());){var o=JL(i.getItemLayout(n));tP(o,i,n,r),o.incremental=!0,this.group.add(o),this._progressiveEls.push(o)}},e.prototype._incrementalRenderLarge=function(t,e){rP(e,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),this._data=null},e.type="candlestick",e}(xg),KL=function(){},$L=function(t){function e(e){var n=t.call(this,e)||this;return n.type="normalCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new KL},e.prototype.buildPath=function(t,e){var n=e.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(gs);function JL(t,e,n){var i=t.ends;return new $L({shape:{points:n?eP(i,t):i},z2:100})}function QL(t,e){for(var n=!0,i=0;i0?"borderColor":"borderColor0"])||n.get(["itemStyle",t>0?"color":"color0"]),o=n.getModel("itemStyle").getItemStyle(jL);e.useStyle(o),e.style.fill=null,e.style.stroke=r}var aP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return n(e,t),e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,e,n){var i=e.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series.candlestick",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(sg);function sP(t){t&&Y(t.series)&&E(t.series,(function(t){q(t)&&"k"===t.type&&(t.type="candlestick")}))}R(aP,EL,!0);var lP=["itemStyle","borderColor"],uP=["itemStyle","borderColor0"],hP=["itemStyle","color"],cP=["itemStyle","color0"],pP={seriesType:"candlestick",plan:yg(),performRawSeries:!0,reset:function(t,e){function n(t,e){return e.get(t>0?hP:cP)}function i(t,e){return e.get(t>0?lP:uP)}if(!e.isSeriesFiltered(t))return!t.pipelineContext.large&&{progress:function(t,e){for(var r;null!=(r=t.next());){var o=e.getItemModel(r),a=e.getItemLayout(r).sign,s=o.getItemStyle();s.fill=n(a,o),s.stroke=i(a,o)||s.fill,A(e.ensureUniqueItemVisual(r,"style"),s)}}}}},dP={seriesType:"candlestick",plan:yg(),reset:function(t){var e=t.coordinateSystem,n=t.getData(),i=function(t,e){var n,i=t.getBaseAxis(),r="category"===i.type?i.getBandWidth():(n=i.getExtent(),Math.abs(n[1]-n[0])/e.count()),o=Er(rt(t.get("barMaxWidth"),r),r),a=Er(rt(t.get("barMinWidth"),1),r),s=t.get("barWidth");return null!=s?Er(s,r):Math.max(Math.min(r/2,o),a)}(t,n),r=["x","y"],o=n.getDimensionIndex(n.mapDimension(r[0])),a=z(n.mapDimensionsAll(r[1]),n.getDimensionIndex,n),s=a[0],l=a[1],u=a[2],h=a[3];if(n.setLayout({candleWidth:i,isSimpleBox:i<=1.3}),!(o<0||a.length<4))return{progress:t.pipelineContext.large?function(t,n){var i,r,a=Sx(4*t.count),c=0,p=[],d=[],f=n.getStore();for(;null!=(r=t.next());){var g=f.get(o,r),y=f.get(s,r),v=f.get(l,r),m=f.get(u,r),x=f.get(h,r);isNaN(g)||isNaN(m)||isNaN(x)?(a[c++]=NaN,c+=3):(a[c++]=fP(f,r,y,v,l),p[0]=g,p[1]=m,i=e.dataToPoint(p,null,d),a[c++]=i?i[0]:NaN,a[c++]=i?i[1]:NaN,p[1]=x,i=e.dataToPoint(p,null,d),a[c++]=i?i[1]:NaN)}n.setLayout("largePoints",a)}:function(t,n){var r,a=n.getStore();for(;null!=(r=t.next());){var c=a.get(o,r),p=a.get(s,r),d=a.get(l,r),f=a.get(u,r),g=a.get(h,r),y=Math.min(p,d),v=Math.max(p,d),m=S(y,c),x=S(v,c),_=S(f,c),b=S(g,c),w=[];M(w,x,0),M(w,m,1),w.push(T(b),T(x),T(_),T(m)),n.setItemLayout(r,{sign:fP(a,r,p,d,l),initBaseline:p>d?x[1]:m[1],ends:w,brushRect:I(f,g,c)})}function S(t,n){var i=[];return i[0]=n,i[1]=t,isNaN(n)||isNaN(t)?[NaN,NaN]:e.dataToPoint(i)}function M(t,e,n){var r=e.slice(),o=e.slice();r[0]=Mh(r[0]+i/2,1,!1),o[0]=Mh(o[0]-i/2,1,!0),n?t.push(r,o):t.push(o,r)}function I(t,e,n){var r=S(t,n),o=S(e,n);return r[0]-=i/2,o[0]-=i/2,{x:r[0],y:r[1],width:i,height:o[1]-r[1]}}function T(t){return t[0]=Mh(t[0],1),t}}}}};function fP(t,e,n,i,r){return n>i?-1:n0?t.get(r,e-1)<=i?1:-1:1}function gP(t,e){var n=e.rippleEffectColor||e.color;t.eachChild((function(t){t.attr({z:e.z,zlevel:e.zlevel,style:{stroke:"stroke"===e.brushType?n:null,fill:"fill"===e.brushType?n:null}})}))}var yP=function(t){function e(e,n){var i=t.call(this)||this,r=new Yw(e,n),o=new Cr;return i.add(r),i.add(o),i.updateData(e,n),i}return n(e,t),e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var e=t.symbolType,n=t.color,i=t.rippleNumber,r=this.childAt(1),o=0;o0&&(o=this._getLineLength(i)/s*1e3),o!==this._period||a!==this._loop){i.stopAnimation();var u=void 0;u=U(l)?l(n):l,i.__t>0&&(u=-o*i.__t),this._animateSymbol(i,o,u,a)}this._period=o,this._loop=a}},e.prototype._animateSymbol=function(t,e,n,i){if(e>0){t.__t=0;var r=this,o=t.animate("",i).when(e,{__t:1}).delay(n).during((function(){r._updateSymbolPosition(t)}));i||o.done((function(){r.remove(t)})),o.start()}},e.prototype._getLineLength=function(t){return Et(t.__p1,t.__cp1)+Et(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,e){t.__p1=e[0],t.__p2=e[1],t.__cp1=e[2]||[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]},e.prototype.updateData=function(t,e,n){this.childAt(0).updateData(t,e,n),this._updateEffectSymbol(t,e)},e.prototype._updateSymbolPosition=function(t){var e=t.__p1,n=t.__p2,i=t.__cp1,r=t.__t,o=[t.x,t.y],a=o.slice(),s=Ke,l=$e;o[0]=s(e[0],i[0],n[0],r),o[1]=s(e[1],i[1],n[1],r);var u=l(e[0],i[0],n[0],r),h=l(e[1],i[1],n[1],r);t.rotation=-Math.atan2(h,u)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==t.__lastT&&t.__lastT=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;oe);o++);o=Math.min(o-1,r-2)}var s=(e-i[o])/(i[o+1]-i[o]),l=n[o],u=n[o+1];t.x=l[0]*(1-s)+s*u[0],t.y=l[1]*(1-s)+s*u[1];var h=u[0]-l[0],c=u[1]-l[1];t.rotation=-Math.atan2(c,h)-Math.PI/2,this._lastFrame=o,this._lastFramePercent=e,t.ignore=!1}},e}(xP),wP=function(){this.polyline=!1,this.curveness=0,this.segs=[]},SP=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new wP},e.prototype.buildPath=function(t,e){var n,i=e.segs,r=e.curveness;if(e.polyline)for(n=this._off;n0){t.moveTo(i[n++],i[n++]);for(var a=1;a0){var c=(s+u)/2-(l-h)*r,p=(l+h)/2-(u-s)*r;t.quadraticCurveTo(c,p,u,h)}else t.lineTo(u,h)}this.incremental&&(this._off=n,this.notClear=!0)},e.prototype.findDataIndex=function(t,e){var n=this.shape,i=n.segs,r=n.curveness,o=this.style.lineWidth;if(n.polyline)for(var a=0,s=0;s0)for(var u=i[s++],h=i[s++],c=1;c0){if(Ja(u,h,(u+p)/2-(h-d)*r,(h+d)/2-(p-u)*r,p,d,o,t,e))return a}else if(Ka(u,h,p,d,o,t,e))return a;a++}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape.segs,n=1/0,i=1/0,r=-1/0,o=-1/0,a=0;a0&&(o.dataIndex=n+t.__startIndex)}))},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),IP={seriesType:"lines",plan:yg(),reset:function(t){var e=t.coordinateSystem;if(e){var n=t.get("polyline"),i=t.pipelineContext.large;return{progress:function(r,o){var a=[];if(i){var s=void 0,l=r.end-r.start;if(n){for(var u=0,h=r.start;h0&&(l||s.configLayer(o,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(a/10+.9,1),0)})),r.updateData(i);var u=t.get("clip",!0)&&lS(t.coordinateSystem,!1,t);u?this.group.setClipPath(u):this.group.removeClipPath(),this._lastZlevel=o,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateLineDraw(i,t).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._lineDraw.incrementalUpdate(t,e.getData()),this._finished=t.end===e.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,e,n){var i=t.getData(),r=t.pipelineContext;if(!this._finished||r.large||r.progressiveRender)return{update:!0};var o=IP.reset(t,e,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,e){var n=this._lineDraw,i=this._showEffect(e),r=!!e.get("polyline"),o=e.pipelineContext.large;return n&&i===this._hasEffet&&r===this._isPolyline&&o===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=o?new MP:new gA(r?i?bP:_P:i?xP:fA),this._hasEffet=i,this._isPolyline=r,this._isLargeDraw=o),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var e=t.getZr();"svg"===e.painter.getType()||null==this._lastZlevel||e.painter.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,e){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(e)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.type="lines",e}(xg),CP="undefined"==typeof Uint32Array?Array:Uint32Array,DP="undefined"==typeof Float64Array?Array:Float64Array;function AP(t){var e=t.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(t.data=z(e,(function(t){var e={coords:[t[0].coord,t[1].coord]};return t[0].name&&(e.fromName=t[0].name),t[1].name&&(e.toName=t[1].name),D([e,t[0],t[1]])})))}var kP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}return n(e,t),e.prototype.init=function(e){e.data=e.data||[],AP(e);var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count)),t.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(e){if(AP(e),e.data){var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count))}t.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var e=this._processFlatCoordsArray(t.data);e.flatCoords&&(this._flatCoords?(this._flatCoords=gt(this._flatCoords,e.flatCoords),this._flatCoordsOffset=gt(this._flatCoordsOffset,e.flatCoordsOffset)):(this._flatCoords=e.flatCoords,this._flatCoordsOffset=e.flatCoordsOffset),t.data=new Float32Array(e.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var e=this.getData().getItemModel(t),n=e.option instanceof Array?e.option:e.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[2*t+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,e){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*t],i=this._flatCoordsOffset[2*t+1],r=0;r ")})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?1e4:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?2e4:this.get("progressiveThreshold"):t},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),e=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&e>0?e+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(sg);function LP(t){return t instanceof Array||(t=[t,t]),t}var PP={seriesType:"lines",reset:function(t){var e=LP(t.get("symbol")),n=LP(t.get("symbolSize")),i=t.getData();return i.setVisual("fromSymbol",e&&e[0]),i.setVisual("toSymbol",e&&e[1]),i.setVisual("fromSymbolSize",n&&n[0]),i.setVisual("toSymbolSize",n&&n[1]),{dataEach:i.hasItemOption?function(t,e){var n=t.getItemModel(e),i=LP(n.getShallow("symbol",!0)),r=LP(n.getShallow("symbolSize",!0));i[0]&&t.setItemVisual(e,"fromSymbol",i[0]),i[1]&&t.setItemVisual(e,"toSymbol",i[1]),r[0]&&t.setItemVisual(e,"fromSymbolSize",r[0]),r[1]&&t.setItemVisual(e,"toSymbolSize",r[1])}:null}}};var OP=function(){function t(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var t=h.createCanvas();this.canvas=t}return t.prototype.update=function(t,e,n,i,r,o){var a=this._getBrush(),s=this._getGradient(r,"inRange"),l=this._getGradient(r,"outOfRange"),u=this.pointSize+this.blurSize,h=this.canvas,c=h.getContext("2d"),p=t.length;h.width=e,h.height=n;for(var d=0;d0){var I=o(v)?s:l;v>0&&(v=v*S+w),x[_++]=I[M],x[_++]=I[M+1],x[_++]=I[M+2],x[_++]=I[M+3]*v*256}else _+=4}return c.putImageData(m,0,0),h},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=h.createCanvas()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();function RP(t){var e=t.dimensions;return"lng"===e[0]&&"lat"===e[1]}var NP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",(function(e){e.eachTargetSeries((function(n){n===t&&(i=e)}))})),this._progressiveEls=null,this.group.removeAll();var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):RP(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(RP(r)?this.render(e,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(e,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){Vh(this._progressiveEls||this.group,t)},e.prototype._renderOnCartesianAndCalendar=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem,h=uS(u,"cartesian2d");if(h){var c=u.getAxis("x"),p=u.getAxis("y");0,o=c.getBandWidth()+.5,a=p.getBandWidth()+.5,s=c.scale.getExtent(),l=p.scale.getExtent()}for(var d=this.group,f=t.getData(),g=t.getModel(["emphasis","itemStyle"]).getItemStyle(),y=t.getModel(["blur","itemStyle"]).getItemStyle(),v=t.getModel(["select","itemStyle"]).getItemStyle(),m=t.get(["itemStyle","borderRadius"]),x=Yh(t),_=t.getModel("emphasis"),b=_.get("focus"),w=_.get("blurScope"),S=_.get("disabled"),M=h?[f.mapDimension("x"),f.mapDimension("y"),f.mapDimension("value")]:[f.mapDimension("time"),f.mapDimension("value")],I=n;Is[1]||Al[1])continue;var k=u.dataToPoint([D,A]);T=new Cs({shape:{x:k[0]-o/2,y:k[1]-a/2,width:o,height:a},style:C})}else{if(isNaN(f.get(M[1],I)))continue;T=new Cs({z2:1,shape:u.dataToRect([f.get(M[0],I)]).contentShape,style:C})}if(f.hasItemOption){var L=f.getItemModel(I),P=L.getModel("emphasis");g=P.getModel("itemStyle").getItemStyle(),y=L.getModel(["blur","itemStyle"]).getItemStyle(),v=L.getModel(["select","itemStyle"]).getItemStyle(),m=L.get(["itemStyle","borderRadius"]),b=P.get("focus"),w=P.get("blurScope"),S=P.get("disabled"),x=Yh(L)}T.shape.r=m;var O=t.getRawValue(I),R="-";O&&null!=O[2]&&(R=O[2]+""),Hh(T,x,{labelFetcher:t,labelDataIndex:I,defaultOpacity:C.opacity,defaultText:R}),T.ensureState("emphasis").style=g,T.ensureState("blur").style=y,T.ensureState("select").style=v,Rl(T,b,w,S),T.incremental=r,r&&(T.states.emphasis.hoverLayer=!0),d.add(T),f.setItemGraphicEl(I,T),this._progressiveEls&&this._progressiveEls.push(T)}},e.prototype._renderOnGeo=function(t,e,n,i){var r=n.targetVisuals.inRange,o=n.targetVisuals.outOfRange,a=e.getData(),s=this._hmLayer||this._hmLayer||new OP;s.blurSize=e.get("blurSize"),s.pointSize=e.get("pointSize"),s.minOpacity=e.get("minOpacity"),s.maxOpacity=e.get("maxOpacity");var l=t.getViewRect().clone(),u=t.getRoamTransform();l.applyTransform(u);var h=Math.max(l.x,0),c=Math.max(l.y,0),p=Math.min(l.width+l.x,i.getWidth()),d=Math.min(l.height+l.y,i.getHeight()),f=p-h,g=d-c,y=[a.mapDimension("lng"),a.mapDimension("lat"),a.mapDimension("value")],v=a.mapArray(y,(function(e,n,i){var r=t.dataToPoint([e,n]);return r[0]-=h,r[1]-=c,r.push(i),r})),m=n.getExtent(),x="visualMap.continuous"===n.type?function(t,e){var n=t[1]-t[0];return e=[(e[0]-t[0])/n,(e[1]-t[0])/n],function(t){return t>=e[0]&&t<=e[1]}}(m,n.option.range):function(t,e,n){var i=t[1]-t[0],r=(e=z(e,(function(e){return{interval:[(e.interval[0]-t[0])/i,(e.interval[1]-t[0])/i]}}))).length,o=0;return function(t){var i;for(i=o;i=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i0?1:-1}(n,o,r,i,c),function(t,e,n,i,r,o,a,s,l,u){var h,c=l.valueDim,p=l.categoryDim,d=Math.abs(n[p.wh]),f=t.getItemVisual(e,"symbolSize");h=Y(f)?f.slice():null==f?["100%","100%"]:[f,f];h[p.index]=Er(h[p.index],d),h[c.index]=Er(h[c.index],i?d:Math.abs(o)),u.symbolSize=h,(u.symbolScale=[h[0]/s,h[1]/s])[c.index]*=(l.isHorizontal?-1:1)*a}(t,e,r,o,0,c.boundingLength,c.pxSign,u,i,c),function(t,e,n,i,r){var o=t.get(zP)||0;o&&(BP.attr({scaleX:e[0],scaleY:e[1],rotation:n}),BP.updateTransform(),o/=BP.getLineScale(),o*=e[i.valueDim.index]);r.valueLineWidth=o||0}(n,c.symbolScale,l,i,c);var p=c.symbolSize,d=Oy(n.get("symbolOffset"),p);return function(t,e,n,i,r,o,a,s,l,u,h,c){var p=h.categoryDim,d=h.valueDim,f=c.pxSign,g=Math.max(e[d.index]+s,0),y=g;if(i){var v=Math.abs(l),m=it(t.get("symbolMargin"),"15%")+"",x=!1;m.lastIndexOf("!")===m.length-1&&(x=!0,m=m.slice(0,m.length-1));var _=Er(m,e[d.index]),b=Math.max(g+2*_,0),w=x?0:2*_,S=eo(i),M=S?i:iO((v+w)/b);b=g+2*(_=(v-M*g)/2/(x?M:Math.max(M-1,1))),w=x?0:2*_,S||"fixed"===i||(M=u?iO((Math.abs(u)+w)/b):0),y=M*b-w,c.repeatTimes=M,c.symbolMargin=_}var I=f*(y/2),T=c.pathPosition=[];T[p.index]=n[p.wh]/2,T[d.index]="start"===a?I:"end"===a?l-I:l/2,o&&(T[0]+=o[0],T[1]+=o[1]);var C=c.bundlePosition=[];C[p.index]=n[p.xy],C[d.index]=n[d.xy];var D=c.barRectShape=A({},n);D[d.wh]=f*Math.max(Math.abs(n[d.wh]),Math.abs(T[d.index]+I)),D[p.wh]=n[p.wh];var k=c.clipShape={};k[p.xy]=-n[p.xy],k[p.wh]=h.ecSize[p.wh],k[d.xy]=0,k[d.wh]=n[d.wh]}(n,p,r,o,0,d,s,c.valueLineWidth,c.boundingLength,c.repeatCutLength,i,c),c}function WP(t,e){return t.toGlobalCoord(t.dataToCoord(t.scale.parse(e)))}function HP(t){var e=t.symbolPatternSize,n=Ly(t.symbolType,-e/2,-e/2,e,e);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function YP(t,e,n,i){var r=t.__pictorialBundle,o=n.symbolSize,a=n.valueLineWidth,s=n.pathPosition,l=e.valueDim,u=n.repeatTimes||0,h=0,c=o[e.valueDim.index]+a+2*n.symbolMargin;for(tO(t,(function(t){t.__pictorialAnimationIndex=h,t.__pictorialRepeatTimes=u,h0:i<0)&&(r=u-1-t),e[l.index]=c*(r-u/2+.5)+s[l.index],{x:e[0],y:e[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function UP(t,e,n,i){var r=t.__pictorialBundle,o=t.__pictorialMainPath;o?eO(o,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(o=t.__pictorialMainPath=HP(n),r.add(o),eO(o,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function XP(t,e,n){var i=A({},e.barRectShape),r=t.__pictorialBarRect;r?eO(r,null,{shape:i},e,n):((r=t.__pictorialBarRect=new Cs({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,t.add(r))}function ZP(t,e,n,i){if(n.symbolClip){var r=t.__pictorialClipPath,o=A({},n.clipShape),a=e.valueDim,s=n.animationModel,l=n.dataIndex;if(r)rh(r,{shape:o},s,l);else{o[a.wh]=0,r=new Cs({shape:o}),t.__pictorialBundle.setClipPath(r),t.__pictorialClipPath=r;var u={};u[a.wh]=n.clipShape[a.wh],Bh[i?"updateProps":"initProps"](r,{shape:u},s,l)}}}function jP(t,e){var n=t.getItemModel(e);return n.getAnimationDelayParams=qP,n.isAnimationEnabled=KP,n}function qP(t){return{index:t.__pictorialAnimationIndex,count:t.__pictorialRepeatTimes}}function KP(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function $P(t,e,n,i){var r=new Cr,o=new Cr;return r.add(o),r.__pictorialBundle=o,o.x=n.bundlePosition[0],o.y=n.bundlePosition[1],n.symbolRepeat?YP(r,e,n):UP(r,0,n),XP(r,n,i),ZP(r,e,n,i),r.__pictorialShapeStr=QP(t,n),r.__pictorialSymbolMeta=n,r}function JP(t,e,n,i){var r=i.__pictorialBarRect;r&&r.removeTextContent();var o=[];tO(i,(function(t){o.push(t)})),i.__pictorialMainPath&&o.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),E(o,(function(t){sh(t,{scaleX:0,scaleY:0},n,e,(function(){i.parent&&i.parent.remove(i)}))})),t.setItemGraphicEl(e,null)}function QP(t,e){return[t.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function tO(t,e,n){E(t.__pictorialBundle.children(),(function(i){i!==t.__pictorialBarRect&&e.call(n,i)}))}function eO(t,e,n,i,r,o){e&&t.attr(e),i.symbolClip&&!r?n&&t.attr(n):n&&Bh[r?"updateProps":"initProps"](t,n,i.animationModel,i.dataIndex,o)}function nO(t,e,n){var i=n.dataIndex,r=n.itemModel,o=r.getModel("emphasis"),a=o.getModel("itemStyle").getItemStyle(),s=r.getModel(["blur","itemStyle"]).getItemStyle(),l=r.getModel(["select","itemStyle"]).getItemStyle(),u=r.getShallow("cursor"),h=o.get("focus"),c=o.get("blurScope"),p=o.get("scale");tO(t,(function(t){if(t instanceof _s){var e=t.style;t.useStyle(A({image:e.image,x:e.x,y:e.y,width:e.width,height:e.height},n.style))}else t.useStyle(n.style);var i=t.ensureState("emphasis");i.style=a,p&&(i.scaleX=1.1*t.scaleX,i.scaleY=1.1*t.scaleY),t.ensureState("blur").style=s,t.ensureState("select").style=l,u&&(t.cursor=u),t.z2=n.z2}));var d=e.valueDim.posDesc[+(n.boundingLength>0)];Hh(t.__pictorialBarRect,Yh(r),{labelFetcher:e.seriesModel,labelDataIndex:i,defaultText:Ww(e.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:d}),Rl(t,h,c,o.get("disabled"))}function iO(t){var e=Math.round(t);return Math.abs(t-e)<1e-4?e:Math.ceil(t)}var rO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}return n(e,t),e.prototype.getInitialData=function(e){return e.stack=null,t.prototype.getInitialData.apply(this,arguments)},e.type="series.pictorialBar",e.dependencies=["grid"],e.defaultOption=yc(IS.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}}),e}(IS);var oO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._layers=[],n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this,o=this.group,a=t.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,u=s.boundaryGap;function h(t){return t.name}o.x=0,o.y=l.y+u[0];var c=new Im(this._layersSeries||[],a,h,h),p=[];function d(e,n,s){var l=r._layers;if("remove"!==e){for(var u,h,c=[],d=[],f=a[n].indices,g=0;go&&(o=s),i.push(s)}for(var u=0;uo&&(o=c)}return{y0:r,max:o}}(l),h=u.y0,c=n/u.max,p=o.length,d=o[0].indices.length,f=0;fMath.PI/2?"right":"left"):S&&"center"!==S?"left"===S?(m=r.r0+w,a>Math.PI/2&&(S="right")):"right"===S&&(m=r.r-w,a>Math.PI/2&&(S="left")):(m=o===2*Math.PI&&0===r.r0?0:(r.r+r.r0)/2,S="center"),g.style.align=S,g.style.verticalAlign=f(p,"verticalAlign")||"middle",g.x=m*s+r.cx,g.y=m*l+r.cy;var M=f(p,"rotate"),I=0;"radial"===M?(I=-a)<-Math.PI/2&&(I+=Math.PI):"tangential"===M?(I=Math.PI/2-a)>Math.PI/2?I-=Math.PI:I<-Math.PI/2&&(I+=Math.PI):j(M)&&(I=M*Math.PI/180),g.rotation=I})),h.dirtyStyle()},e}(Cu),hO="sunburstRootToNode",cO="sunburstHighlight";var pO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this;this.seriesModel=t,this.api=n,this.ecModel=e;var o=t.getData(),a=o.tree.root,s=t.getViewRoot(),l=this.group,u=t.get("renderLabelForZeroData"),h=[];s.eachNode((function(t){h.push(t)}));var c=this._oldChildren||[];!function(i,r){if(0===i.length&&0===r.length)return;function s(t){return t.getId()}function h(s,h){!function(i,r){u||!i||i.getValue()||(i=null);if(i!==a&&r!==a)if(r&&r.piece)i?(r.piece.updateData(!1,i,t,e,n),o.setItemGraphicEl(i.dataIndex,r.piece)):function(t){if(!t)return;t.piece&&(l.remove(t.piece),t.piece=null)}(r);else if(i){var s=new uO(i,t,e,n);l.add(s),o.setItemGraphicEl(i.dataIndex,s)}}(null==s?null:i[s],null==h?null:r[h])}new Im(r,i,s,s).add(h).update(h).remove(H(h,null)).execute()}(h,c),function(i,o){o.depth>0?(r.virtualPiece?r.virtualPiece.updateData(!1,i,t,e,n):(r.virtualPiece=new uO(i,t,e,n),l.add(r.virtualPiece)),o.piece.off("click"),r.virtualPiece.on("click",(function(t){r._rootToNode(o.parentNode)}))):r.virtualPiece&&(l.remove(r.virtualPiece),r.virtualPiece=null)}(a,s),this._initEvents(),this._oldChildren=h},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",(function(e){var n=!1;t.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===e.target){var r=i.getModel().get("nodeClick");if("rootToNode"===r)t._rootToNode(i);else if("link"===r){var o=i.getModel(),a=o.get("link");if(a)dp(a,o.get("target",!0)||"_blank")}n=!0}}))}))},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:hO,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}},e.type="sunburst",e}(xg),dO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreStyleOnData=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};fO(n);var i=this._levelModels=z(t.levels||[],(function(t){return new dc(t,this,e)}),this),r=AC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e),o=i[n.depth];return o&&(t.parentModel=o),t}))}));return r.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treePathInfo=OC(i,this),n},e.prototype.getLevelModel=function(t){return this._levelModels&&this._levelModels[t.depth]},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){BC(this)},e.type="series.sunburst",e.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"},e}(sg);function fO(t){var e=0;E(t.children,(function(t){fO(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var gO=Math.PI/180;function yO(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.get("center"),i=t.get("radius");Y(i)||(i=[0,i]),Y(e)||(e=[e,e]);var r=n.getWidth(),o=n.getHeight(),a=Math.min(r,o),s=Er(e[0],r),l=Er(e[1],o),u=Er(i[0],a/2),h=Er(i[1],a/2),c=-t.get("startAngle")*gO,p=t.get("minAngle")*gO,d=t.getData().tree.root,f=t.getViewRoot(),g=f.depth,y=t.get("sort");null!=y&&vO(f,y);var v=0;E(f.children,(function(t){!isNaN(t.getValue())&&v++}));var m=f.getValue(),x=Math.PI/(m||v)*2,_=f.depth>0,b=f.height-(_?-1:1),w=(h-u)/(b||1),S=t.get("clockwise"),M=t.get("stillShowZeroSum"),I=S?1:-1,T=function(e,n){if(e){var i=n;if(e!==d){var r=e.getValue(),o=0===m&&M?x:r*x;o1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&X(o)&&(o=Sn(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),A(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)}))}))}var xO={color:"fill",borderColor:"stroke"},_O={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},bO=So(),wO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},e.prototype.getInitialData=function(t,e){return rx(null,this)},e.prototype.getDataParams=function(e,n,i){var r=t.prototype.getDataParams.call(this,e,n);return i&&(r.info=bO(i).info),r},e.type="series.custom",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},e}(sg);function SO(t,e){return e=e||[0,0],z(["x","y"],(function(n,i){var r=this.getAxis(n),o=e[i],a=t[i]/2;return"category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a))}),this)}function MO(t,e){return e=e||[0,0],z([0,1],(function(n){var i=e[n],r=t[n]/2,o=[],a=[];return o[n]=i-r,a[n]=i+r,o[1-n]=a[1-n]=e[1-n],Math.abs(this.dataToPoint(o)[n]-this.dataToPoint(a)[n])}),this)}function IO(t,e){var n=this.getAxis(),i=e instanceof Array?e[0]:e,r=(t instanceof Array?t[0]:t)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-r)-n.dataToCoord(i+r))}function TO(t,e){return e=e||[0,0],z(["Radius","Angle"],(function(n,i){var r=this["get"+n+"Axis"](),o=e[i],a=t[i]/2,s="category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function CO(t,e,n,i){return t&&(t.legacy||!1!==t.legacy&&!n&&!i&&"tspan"!==e&&("text"===e||mt(t,"text")))}function DO(t,e,n){var i,r,o,a=t;if("text"===e)o=a;else{o={},mt(a,"text")&&(o.text=a.text),mt(a,"rich")&&(o.rich=a.rich),mt(a,"textFill")&&(o.fill=a.textFill),mt(a,"textStroke")&&(o.stroke=a.textStroke),mt(a,"fontFamily")&&(o.fontFamily=a.fontFamily),mt(a,"fontSize")&&(o.fontSize=a.fontSize),mt(a,"fontStyle")&&(o.fontStyle=a.fontStyle),mt(a,"fontWeight")&&(o.fontWeight=a.fontWeight),r={type:"text",style:o,silent:!0},i={};var s=mt(a,"textPosition");n?i.position=s?a.textPosition:"inside":s&&(i.position=a.textPosition),mt(a,"textPosition")&&(i.position=a.textPosition),mt(a,"textOffset")&&(i.offset=a.textOffset),mt(a,"textRotation")&&(i.rotation=a.textRotation),mt(a,"textDistance")&&(i.distance=a.textDistance)}return AO(o,t),E(o.rich,(function(t){AO(t,t)})),{textConfig:i,textContent:r}}function AO(t,e){e&&(e.font=e.textFont||e.font,mt(e,"textStrokeWidth")&&(t.lineWidth=e.textStrokeWidth),mt(e,"textAlign")&&(t.align=e.textAlign),mt(e,"textVerticalAlign")&&(t.verticalAlign=e.textVerticalAlign),mt(e,"textLineHeight")&&(t.lineHeight=e.textLineHeight),mt(e,"textWidth")&&(t.width=e.textWidth),mt(e,"textHeight")&&(t.height=e.textHeight),mt(e,"textBackgroundColor")&&(t.backgroundColor=e.textBackgroundColor),mt(e,"textPadding")&&(t.padding=e.textPadding),mt(e,"textBorderColor")&&(t.borderColor=e.textBorderColor),mt(e,"textBorderWidth")&&(t.borderWidth=e.textBorderWidth),mt(e,"textBorderRadius")&&(t.borderRadius=e.textBorderRadius),mt(e,"textBoxShadowColor")&&(t.shadowColor=e.textBoxShadowColor),mt(e,"textBoxShadowBlur")&&(t.shadowBlur=e.textBoxShadowBlur),mt(e,"textBoxShadowOffsetX")&&(t.shadowOffsetX=e.textBoxShadowOffsetX),mt(e,"textBoxShadowOffsetY")&&(t.shadowOffsetY=e.textBoxShadowOffsetY))}function kO(t,e,n){var i=t;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var r=i.textPosition.indexOf("inside")>=0,o=t.fill||"#000";LO(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,E(e.rich,(function(t){LO(t,t)})),i}function LO(t,e){e&&(mt(e,"fill")&&(t.textFill=e.fill),mt(e,"stroke")&&(t.textStroke=e.fill),mt(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),mt(e,"font")&&(t.font=e.font),mt(e,"fontStyle")&&(t.fontStyle=e.fontStyle),mt(e,"fontWeight")&&(t.fontWeight=e.fontWeight),mt(e,"fontSize")&&(t.fontSize=e.fontSize),mt(e,"fontFamily")&&(t.fontFamily=e.fontFamily),mt(e,"align")&&(t.textAlign=e.align),mt(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),mt(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),mt(e,"width")&&(t.textWidth=e.width),mt(e,"height")&&(t.textHeight=e.height),mt(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),mt(e,"padding")&&(t.textPadding=e.padding),mt(e,"borderColor")&&(t.textBorderColor=e.borderColor),mt(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),mt(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),mt(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),mt(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),mt(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),mt(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),mt(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),mt(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),mt(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),mt(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}var PO={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},OO=G(PO),RO=(V(Ki,(function(t,e){return t[e]=1,t}),{}),Ki.join(", "),["","style","shape","extra"]),NO=So();function EO(t,e,n,i,r){var o=t+"Animation",a=nh(t,i,r)||{},s=NO(e).userDuring;return a.duration>0&&(a.during=s?W(HO,{el:e,userDuring:s}):null,a.setToFinal=!0,a.scope=t),A(a,n[o]),a}function zO(t,e,n,i){var r=(i=i||{}).dataIndex,o=i.isInit,a=i.clearStyle,s=n.isAnimationEnabled(),l=NO(t),u=e.style;l.userDuring=e.during;var h={},c={};if(function(t,e,n){for(var i=0;i=0)){var c=t.getAnimationStyleProps(),p=c?c.style:null;if(p){!r&&(r=i.style={});var d=G(n);for(u=0;u0&&t.animateFrom(p,d)}else!function(t,e,n,i,r){if(r){var o=EO("update",t,e,i,n);o.duration>0&&t.animateFrom(r,o)}}(t,e,r||0,n,h);VO(t,e),u?t.dirty():t.markRedraw()}function VO(t,e){for(var n=NO(t).leaveToProps,i=0;i=0){!o&&(o=i[t]={});var p=G(a);for(h=0;hi[1]&&i.reverse(),{coordSys:{type:"polar",cx:t.cx,cy:t.cy,r:i[1],r0:i[0]},api:{coord:function(i){var r=e.dataToRadius(i[0]),o=n.dataToAngle(i[1]),a=t.coordToPoint([r,o]);return a.push(r,o*Math.PI/180),a},size:W(TO,t)}}},calendar:function(t){var e=t.getRect(),n=t.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:t.getCellWidth(),cellHeight:t.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(e,n){return t.dataToPoint(e,n)}}}}};function sR(t){return t instanceof gs}function lR(t){return t instanceof da}var uR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._data,o=t.getData(),a=this.group,s=fR(t,o,e,n);r||a.removeAll(),o.diff(r).add((function(e){yR(n,null,e,s(e,i),t,a,o)})).remove((function(e){var n=r.getItemGraphicEl(e);BO(n,bO(n).option,t)})).update((function(e,l){var u=r.getItemGraphicEl(l);yR(n,u,e,s(e,i),t,a,o)})).execute();var l=t.get("clip",!0)?lS(t.coordinateSystem,!1,t):null;l?a.setClipPath(l):a.removeClipPath(),this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll(),this._data=null},e.prototype.incrementalRender=function(t,e,n,i,r){var o=e.getData(),a=fR(e,o,n,i),s=this._progressiveEls=[];function l(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}for(var u=t.start;u=0?e.getStore().get(r,n):void 0}var o=e.get(i.name,n),a=i&&i.ordinalMeta;return a?a.categories[o]:o},styleEmphasis:function(n,i){0;null==i&&(i=s);var r=m(i,$O).getItemStyle(),o=x(i,$O),a=Uh(o,null,null,!0,!0);a.text=o.getShallow("show")?ot(t.getFormattedLabel(i,$O),t.getFormattedLabel(i,JO),Ww(e,i)):null;var l=Xh(o,null,!0);return b(n,r),r=kO(r,a,l),n&&_(r,n),r.legacy=!0,r},visual:function(t,n){if(null==n&&(n=s),mt(xO,t)){var i=e.getItemVisual(n,"style");return i?i[xO[t]]:null}if(mt(_O,t))return e.getItemVisual(n,t)},barLayout:function(t){if("cartesian2d"===o.type){return function(t){var e=[],n=t.axis,i="axis0";if("category"===n.type){for(var r=n.getBandWidth(),o=0;o=c;p--){BO(e.childAt(p),bO(e).option,r)}}(t,c,n,i,r),a>=0?o.replaceAt(c,a):o.add(c),c}function mR(t,e,n){var i,r=bO(t),o=e.type,a=e.shape,s=e.style;return n.isUniversalTransitionEnabled()||null!=o&&o!==r.customGraphicType||"path"===o&&((i=a)&&(mt(i,"pathData")||mt(i,"d")))&&IR(a)!==r.customPathData||"image"===o&&mt(s,"image")&&s.image!==r.customImagePath}function xR(t,e,n){var i=e?_R(t,e):t,r=e?bR(t,i,$O):t.style,o=t.type,a=i?i.textConfig:null,s=t.textContent,l=s?e?_R(s,e):s:null;if(r&&(n.isLegacy||CO(r,o,!!a,!!l))){n.isLegacy=!0;var u=DO(r,o,!e);!a&&u.textConfig&&(a=u.textConfig),!l&&u.textContent&&(l=u.textContent)}if(!e&&l){var h=l;!h.type&&(h.type="text")}var c=e?n[e]:n.normal;c.cfg=a,c.conOpt=l}function _R(t,e){return e?t?t[e]:null:t}function bR(t,e,n){var i=e&&e.style;return null==i&&n===$O&&t&&(i=t.styleEmphasis),i}function wR(t,e){var n=t&&t.name;return null!=n?n:"e\0\0"+e}function SR(t,e){var n=this.context,i=null!=t?n.newChildren[t]:null,r=null!=e?n.oldChildren[e]:null;vR(n.api,r,n.dataIndex,i,n.seriesModel,n.group)}function MR(t){var e=this.context,n=e.oldChildren[t];BO(n,bO(n).option,e.seriesModel)}function IR(t){return t&&(t.pathData||t.d)}var TR=So(),CR=T,DR=W,AR=function(){function t(){this._dragging=!1,this.animationThreshold=15}return t.prototype.render=function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var h=this._moveAnimation=this.determineAnimation(t,e);if(a){var c=H(kR,e,h);this.updatePointerEl(a,l,c),this.updateLabelEl(a,l,c,e)}else a=this._group=new Cr,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);RR(a,e,!0),this._renderHandle(r)}},t.prototype.remove=function(t){this.clear(t)},t.prototype.dispose=function(t){this.clear(t)},t.prototype.determineAnimation=function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&i.getBandWidth()>a)return!0;if(o){var s=KM(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},t.prototype.makeElOption=function(t,e,n,i,r){},t.prototype.createPointerEl=function(t,e,n,i){var r=e.pointer;if(r){var o=TR(t).pointerEl=new Bh[r.type](CR(e.pointer));t.add(o)}},t.prototype.createLabelEl=function(t,e,n,i){if(e.label){var r=TR(t).labelEl=new ks(CR(e.label));t.add(r),PR(r,i)}},t.prototype.updatePointerEl=function(t,e,n){var i=TR(t).pointerEl;i&&e.pointer&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},t.prototype.updateLabelEl=function(t,e,n,i){var r=TR(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{x:e.label.x,y:e.label.y}),PR(r,i))},t.prototype._renderHandle=function(t){if(!this._dragging&&this.updateHandleTransform){var e,n=this._axisPointerModel,i=this._api.getZr(),r=this._handle,o=n.getModel("handle"),a=n.get("status");if(!o.get("show")||!a||"hide"===a)return r&&i.remove(r),void(this._handle=null);this._handle||(e=!0,r=this._handle=Ph(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){se(t.event)},onmousedown:DR(this._onHandleDragMove,this,0,0),drift:DR(this._onHandleDragMove,this),ondragend:DR(this._onHandleDragEnd,this)}),i.add(r)),RR(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");Y(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,Ag(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,e)}},t.prototype._moveHandleToValue=function(t,e){kR(this._axisPointerModel,!e&&this._moveAnimation,this._handle,OR(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},t.prototype._onHandleDragMove=function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(OR(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(OR(i)),TR(n).lastProp=null,this._doDispatchAxisPointer()}},t.prototype._doDispatchAxisPointer=function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},t.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},t.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),kg(this,"_doDispatchAxisPointer")},t.prototype.doClear=function(){},t.prototype.buildLabel=function(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}},t}();function kR(t,e,n,i){LR(TR(n).lastProp,i)||(TR(n).lastProp=i,e?rh(n,i,t):(n.stopAnimation(),n.attr(i)))}function LR(t,e){if(q(t)&&q(e)){var n=!0;return E(e,(function(e,i){n=n&&LR(t[i],e)})),!!n}return t===e}function PR(t,e){t[e.get(["label","show"])?"show":"hide"]()}function OR(t){return{x:t.x||0,y:t.y||0,rotation:t.rotation||0}}function RR(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse((function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)}))}function NR(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function ER(t,e,n,i,r){var o=zR(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),a=n.getModel("label"),s=ip(a.get("padding")||0),l=a.getFont(),u=cr(o,l),h=r.position,c=u.width+s[1]+s[3],p=u.height+s[0]+s[2],d=r.align;"right"===d&&(h[0]-=c),"center"===d&&(h[0]-=c/2);var f=r.verticalAlign;"bottom"===f&&(h[1]-=p),"middle"===f&&(h[1]-=p/2),function(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}(h,c,p,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get(["axisLine","lineStyle","color"])),t.label={x:h[0],y:h[1],style:Uh(a,{text:o,font:l,fill:a.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function zR(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel({value:t},{precision:r.precision}),a=r.formatter;if(a){var s={value:s_(e,{value:t}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};E(i,(function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)})),X(a)?o=a.replace("{value}",o):U(a)&&(o=a(s))}return o}function VR(t,e,n){var i=[1,0,0,1,0,0];return zi(i,i,n.rotation),Ei(i,i,n.position),Th([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function BR(t,e,n,i,r,o){var a=GM.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),ER(e,i,r,o,{position:VR(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function FR(t,e,n){return{x1:t[n=n||0],y1:t[1-n],x2:e[n],y2:e[1-n]}}function GR(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}}function WR(t,e,n,i,r,o){return{cx:t,cy:e,r0:n,r:i,startAngle:r,endAngle:o,clockwise:!0}}var HR=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=YR(a,o).getOtherAxis(o).getGlobalExtent(),u=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=NR(i),c=UR[s](o,u,l);c.style=h,t.graphicKey=c.type,t.pointer=c}BR(e,t,LM(a.model,n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=LM(e.axis.grid.model,e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=VR(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=YR(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=[t.x,t.y];u[l]+=e[l],u[l]=Math.min(a[1],u[l]),u[l]=Math.max(a[0],u[l]);var h=(s[1]+s[0])/2,c=[h,h];c[l]=u[l];return{x:u[0],y:u[1],rotation:t.rotation,cursorPoint:c,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},e}(AR);function YR(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}var UR={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:FR([e,n[0]],[e,n[1]],XR(t))}},shadow:function(t,e,n){var i=Math.max(1,t.getBandWidth()),r=n[1]-n[0];return{type:"Rect",shape:GR([e-i/2,n[0]],[i,r],XR(t))}}};function XR(t){return"x"===t.dim?0:1}var ZR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},e}(Tp),jR=So(),qR=E;function KR(t,e,n){if(!r.node){var i=e.getZr();jR(i).records||(jR(i).records={}),function(t,e){if(jR(t).initialized)return;function n(n,i){t.on(n,(function(n){var r=function(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}(e);qR(jR(t).records,(function(t){t&&i(t,n,r.dispatchAction)})),function(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]);n&&(n.dispatchAction=null,e.dispatchAction(n))}(r.pendings,e)}))}jR(t).initialized=!0,n("click",H(JR,"click")),n("mousemove",H(JR,"mousemove")),n("globalout",$R)}(i,e),(jR(i).records[t]||(jR(i).records[t]={})).handler=n}}function $R(t,e,n){t.handler("leave",null,n)}function JR(t,e,n,i){e.handler(t,n,i)}function QR(t,e){if(!r.node){var n=e.getZr();(jR(n).records||{})[t]&&(jR(n).records[t]=null)}}var tN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";KR("axisPointer",n,(function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})}))},e.prototype.remove=function(t,e){QR("axisPointer",e)},e.prototype.dispose=function(t,e){QR("axisPointer",e)},e.type="axisPointer",e}(gg);function eN(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=wo(o,t);if(null==a||a<0||Y(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)if(t.isStacked){var u=l.getBaseAxis(),h=l.getOtherAxis(u).dim,c=u.dim,p="x"===h||"radius"===h?1:0,d=o.mapDimension(c),f=[];f[p]=o.get(d,a),f[1-p]=o.get(o.getCalculationInfo("stackResultDimension"),a),i=l.dataToPoint(f)||[]}else i=l.dataToPoint(o.getValues(z(l.dimensions,(function(t){return o.mapDimension(t)})),a))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var nN=So();function iN(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||W(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){lN(r)&&(r=eN({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=lN(r),u=o.axesInfo,h=s.axesInfo,c="leave"===i||lN(r),p={},d={},f={list:[],map:{}},g={showPointer:H(oN,d),showTooltip:H(aN,f)};E(s.coordSysMap,(function(t,e){var n=l||t.containPoint(r);E(s.coordSysAxesInfo[e],(function(t,e){var i=t.axis,o=function(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}(u,t);if(!c&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&rN(t,a,g,!1,p)}}))}));var y={};return E(h,(function(t,e){var n=t.linkGroup;n&&!d[e]&&E(n.axesInfo,(function(e,i){var r=d[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,sN(e),sN(t)))),y[t.key]=o}}))})),E(y,(function(t,e){rN(h[e],t,g,!0,p)})),function(t,e,n){var i=n.axesInfo=[];E(e,(function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})}))}(d,h,p),function(t,e,n,i){if(lN(e)||!t.list.length)return void i({type:"hideTip"});var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}(f,r,t,a),function(t,e,n){var i=n.getZr(),r="axisPointerLastHighlights",o=nN(i)[r]||{},a=nN(i)[r]={};E(t,(function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&E(n.seriesDataIndices,(function(t){var e=t.seriesIndex+" | "+t.dataIndex;a[e]=t}))}));var s=[],l=[];E(o,(function(t,e){!a[e]&&l.push(t)})),E(a,(function(t,e){!o[e]&&s.push(t)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(h,0,n),p}}function rN(t,e,n,i,r){var o=t.axis;if(!o.scale.isBlank()&&o.containData(e))if(t.involveSeries){var a=function(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return E(e.seriesModels,(function(e,l){var u,h,c=e.getData().mapDimensionsAll(i);if(e.getAxisTooltipData){var p=e.getAxisTooltipData(c,t,n);h=p.dataIndices,u=p.nestestValue}else{if(!(h=e.getData().indicesOfNearest(c[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(c[0],h[0])}if(null!=u&&isFinite(u)){var d=t-u,f=Math.abs(d);f<=a&&((f=0&&s<0)&&(a=f,s=d,r=u,o.length=0),E(h,(function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})})))}})),{payloadBatch:o,snapToValue:r}}(e,t),s=a.payloadBatch,l=a.snapToValue;s[0]&&null==r.seriesIndex&&A(r,s[0]),!i&&t.snap&&o.containData(l)&&null!=l&&(e=l),n.showPointer(t,e,s),n.showTooltip(t,a,l)}else n.showPointer(t,e)}function oN(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function aN(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=JM(l),h=t.map[u];h||(h=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(h)),h.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function sN(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function lN(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function uN(t){tI.registerAxisPointerClass("CartesianAxisPointer",HR),t.registerComponentModel(ZR),t.registerComponentView(tN),t.registerPreprocessor((function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!Y(e)&&(t.axisPointer.link=[e])}})),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,(function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=ZM(t,e)})),t.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},iN)}var hN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis;"angle"===o.dim&&(this.animationThreshold=Math.PI/18);var a=o.polar,s=a.getOtherAxis(o).getExtent(),l=o.dataToCoord(e),u=i.get("type");if(u&&"none"!==u){var h=NR(i),c=cN[u](o,a,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}var p=function(t,e,n,i,r){var o=e.axis,a=o.dataToCoord(t),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,u,h,c=i.getRadiusAxis().getExtent();if("radius"===o.dim){var p=[1,0,0,1,0,0];zi(p,p,s),Ei(p,p,[i.cx,i.cy]),l=Th([a,-r],p);var d=e.getModel("axisLabel").get("rotate")||0,f=GM.innerTextLayout(s,d*Math.PI/180,-1);u=f.textAlign,h=f.textVerticalAlign}else{var g=c[1];l=i.coordToPoint([g+r,a]);var y=i.cx,v=i.cy;u=Math.abs(l[0]-y)/g<.3?"center":l[0]>y?"left":"right",h=Math.abs(l[1]-v)/g<.3?"middle":l[1]>v?"top":"bottom"}return{position:l,align:u,verticalAlign:h}}(e,n,0,a,i.get(["label","margin"]));ER(t,n,i,r,p)},e}(AR);var cN={line:function(t,e,n,i){return"angle"===t.dim?{type:"Line",shape:FR(e.coordToPoint([i[0],n]),e.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:n}}},shadow:function(t,e,n,i){var r=Math.max(1,t.getBandWidth()),o=Math.PI/180;return"angle"===t.dim?{type:"Sector",shape:WR(e.cx,e.cy,i[0],i[1],(-n-r/2)*o,(r/2-n)*o)}:{type:"Sector",shape:WR(e.cx,e.cy,n-r/2,n+r/2,0,2*Math.PI)}}},pN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.findAxisModel=function(t){var e;return this.ecModel.eachComponent(t,(function(t){t.getCoordSysModel()===this&&(e=t)}),this),e},e.type="polar",e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(Tp),dN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Co).models[0]},e.type="polarAxis",e}(Tp);R(dN,p_);var fN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="angleAxis",e}(dN),gN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="radiusAxis",e}(dN),yN=function(t){function e(e,n){return t.call(this,"radius",e,n)||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e}(H_);yN.prototype.dataToRadius=H_.prototype.dataToCoord,yN.prototype.radiusToData=H_.prototype.coordToData;var vN=So(),mN=function(t){function e(e,n){return t.call(this,"angle",e,n||[0,360])||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,e=t.getLabelModel(),n=t.scale,i=n.getExtent(),r=n.count();if(i[1]-i[0]<1)return 0;var o=i[0],a=t.dataToCoord(o+1)-t.dataToCoord(o),s=Math.abs(a),l=cr(null==o?"":o+"",e.getFont(),"center","top"),u=Math.max(l.height,7)/s;isNaN(u)&&(u=1/0);var h=Math.max(0,Math.floor(u)),c=vN(t.model),p=c.lastAutoInterval,d=c.lastTickCount;return null!=p&&null!=d&&Math.abs(p-h)<=1&&Math.abs(d-r)<=1&&p>h?h=p:(c.lastTickCount=r,c.lastAutoInterval=h),h},e}(H_);mN.prototype.dataToAngle=H_.prototype.dataToCoord,mN.prototype.angleToData=H_.prototype.coordToData;var xN=["radius","angle"],_N=function(){function t(t){this.dimensions=xN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new yN,this._angleAxis=new mN,this.axisPointerEnabled=!0,this.name=t||"",this._radiusAxis.polar=this._angleAxis.polar=this}return t.prototype.containPoint=function(t){var e=this.pointToCoord(t);return this._radiusAxis.contain(e[0])&&this._angleAxis.contain(e[1])},t.prototype.containData=function(t){return this._radiusAxis.containData(t[0])&&this._angleAxis.containData(t[1])},t.prototype.getAxis=function(t){return this["_"+t+"Axis"]},t.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},t.prototype.getAxesByScale=function(t){var e=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===t&&e.push(n),i.scale.type===t&&e.push(i),e},t.prototype.getAngleAxis=function(){return this._angleAxis},t.prototype.getRadiusAxis=function(){return this._radiusAxis},t.prototype.getOtherAxis=function(t){var e=this._angleAxis;return t===e?this._radiusAxis:e},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},t.prototype.getTooltipAxes=function(t){var e=null!=t&&"auto"!==t?this.getAxis(t):this.getBaseAxis();return{baseAxes:[e],otherAxes:[this.getOtherAxis(e)]}},t.prototype.dataToPoint=function(t,e){return this.coordToPoint([this._radiusAxis.dataToRadius(t[0],e),this._angleAxis.dataToAngle(t[1],e)])},t.prototype.pointToData=function(t,e){var n=this.pointToCoord(t);return[this._radiusAxis.radiusToData(n[0],e),this._angleAxis.angleToData(n[1],e)]},t.prototype.pointToCoord=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=this.getAngleAxis(),r=i.getExtent(),o=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);i.inverse?o=a-360:a=o+360;var s=Math.sqrt(e*e+n*n);e/=s,n/=s;for(var l=Math.atan2(-n,e)/Math.PI*180,u=la;)l+=360*u;return[s,l]},t.prototype.coordToPoint=function(t){var e=t[0],n=t[1]/180*Math.PI;return[Math.cos(n)*e+this.cx,-Math.sin(n)*e+this.cy]},t.prototype.getArea=function(){var t=this.getAngleAxis(),e=this.getRadiusAxis().getExtent().slice();e[0]>e[1]&&e.reverse();var n=t.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:e[0],r:e[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:t.inverse,contain:function(t,e){var n=t-this.cx,i=e-this.cy,r=n*n+i*i-1e-4,o=this.r,a=this.r0;return r<=o*o&&r>=a*a}}},t.prototype.convertToPixel=function(t,e,n){return bN(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return bN(e)===this?this.pointToData(n):null},t}();function bN(t){var e=t.seriesModel,n=t.polarModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}function wN(t,e){var n=this,i=n.getAngleAxis(),r=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),r.scale.setExtent(1/0,-1/0),t.eachSeries((function(t){if(t.coordinateSystem===n){var e=t.getData();E(c_(e,"radius"),(function(t){r.scale.unionExtentFromData(e,t)})),E(c_(e,"angle"),(function(t){i.scale.unionExtentFromData(e,t)}))}})),r_(i.scale,i.model),r_(r.scale,r.model),"category"===i.type&&!i.onBand){var o=i.getExtent(),a=360/i.scale.count();i.inverse?o[1]+=a:o[1]-=a,i.setExtent(o[0],o[1])}}function SN(t,e){if(t.type=e.get("type"),t.scale=o_(e),t.onBand=e.get("boundaryGap")&&"category"===t.type,t.inverse=e.get("inverse"),function(t){return"angleAxis"===t.mainType}(e)){t.inverse=t.inverse!==e.get("clockwise");var n=e.get("startAngle");t.setExtent(n,n+(t.inverse?-360:360))}e.axis=t,t.model=e}var MN={dimensions:xN,create:function(t,e){var n=[];return t.eachComponent("polar",(function(t,i){var r=new _N(i+"");r.update=wN;var o=r.getRadiusAxis(),a=r.getAngleAxis(),s=t.findAxisModel("radiusAxis"),l=t.findAxisModel("angleAxis");SN(o,s),SN(a,l),function(t,e,n){var i=e.get("center"),r=n.getWidth(),o=n.getHeight();t.cx=Er(i[0],r),t.cy=Er(i[1],o);var a=t.getRadiusAxis(),s=Math.min(r,o)/2,l=e.get("radius");null==l?l=[0,"100%"]:Y(l)||(l=[0,l]);var u=[Er(l[0],s),Er(l[1],s)];a.inverse?a.setExtent(u[1],u[0]):a.setExtent(u[0],u[1])}(r,t,e),n.push(r),t.coordinateSystem=r,r.model=t})),t.eachSeries((function(t){if("polar"===t.get("coordinateSystem")){var e=t.getReferringComponents("polar",Co).models[0];0,t.coordinateSystem=e.coordinateSystem}})),n}},IN=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function TN(t,e,n){e[1]>e[0]&&(e=e.slice().reverse());var i=t.coordToPoint([e[0],n]),r=t.coordToPoint([e[1],n]);return{x1:i[0],y1:i[1],x2:r[0],y2:r[1]}}function CN(t){return t.getRadiusAxis().inverse?0:1}function DN(t){var e=t[0],n=t[t.length-1];e&&n&&Math.abs(Math.abs(e.coord-n.coord)-360)<1e-4&&t.pop()}var AN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="PolarAxisPointer",n}return n(e,t),e.prototype.render=function(t,e){if(this.group.removeAll(),t.get("show")){var n=t.axis,i=n.polar,r=i.getRadiusAxis().getExtent(),o=n.getTicksCoords(),a=n.getMinorTicksCoords(),s=z(n.getViewLabels(),(function(t){t=T(t);var e=n.scale,i="ordinal"===e.type?e.getRawOrdinalNumber(t.tickValue):t.tickValue;return t.coord=n.dataToCoord(i),t}));DN(s),DN(o),E(IN,(function(e){!t.get([e,"show"])||n.scale.isBlank()&&"axisLine"!==e||kN[e](this.group,t,i,o,a,r,s)}),this)}},e.type="angleAxis",e}(tI),kN={axisLine:function(t,e,n,i,r,o){var a,s=e.getModel(["axisLine","lineStyle"]),l=CN(n),u=l?0:1;(a=0===o[u]?new hu({shape:{cx:n.cx,cy:n.cy,r:o[l]},style:s.getLineStyle(),z2:1,silent:!0}):new Au({shape:{cx:n.cx,cy:n.cy,r:o[l],r0:o[u]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,t.add(a)},axisTick:function(t,e,n,i,r,o){var a=e.getModel("axisTick"),s=(a.get("inside")?-1:1)*a.get("length"),l=o[CN(n)],u=z(i,(function(t){return new zu({shape:TN(n,[l,l+s],t.coord)})}));t.add(wh(u,{style:k(a.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(a.get("inside")?-1:1)*s.get("length"),u=o[CN(n)],h=[],c=0;cf?"left":"right",v=Math.abs(d[1]-g)/p<.3?"middle":d[1]>g?"top":"bottom";if(s&&s[c]){var m=s[c];q(m)&&m.textStyle&&(a=new dc(m.textStyle,l,l.ecModel))}var x=new ks({silent:GM.isLabelSilent(e),style:Uh(a,{x:d[0],y:d[1],fill:a.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:y,verticalAlign:v})});if(t.add(x),h){var _=GM.makeAxisEventDataBase(e);_.targetType="axisLabel",_.value=i.rawLabel,Hs(x).eventData=_}}),this)},splitLine:function(t,e,n,i,r,o){var a=e.getModel("splitLine").getModel("lineStyle"),s=a.get("color"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=0;h=0?"p":"n",T=_;m&&(i[s][M]||(i[s][M]={p:_,n:_}),T=i[s][M][I]);var C=void 0,D=void 0,A=void 0,k=void 0;if("radius"===c.dim){var L=c.dataToCoord(S)-_,P=o.dataToCoord(M);Math.abs(L)=k})}}}))}var VN={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},BN={splitNumber:5},FN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="polar",e}(gg);function GN(t,e){e=e||{};var n=t.coordinateSystem,i=t.axis,r={},o=i.position,a=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};r.position=["vertical"===a?u.vertical[o]:l[0],"horizontal"===a?u.horizontal[o]:l[3]];r.rotation=Math.PI/2*{horizontal:0,vertical:1}[a];r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,right:1,left:-1}[o],t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),it(e.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var h=e.rotate;return null==h&&(h=t.get(["axisLabel","rotate"])),r.labelRotation="top"===o?-h:h,r.z2=1,r}var WN=["axisLine","axisTickLabel","axisName"],HN=["splitArea","splitLine"],YN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="SingleAxisPointer",n}return n(e,t),e.prototype.render=function(e,n,i,r){var o=this.group;o.removeAll();var a=this._axisGroup;this._axisGroup=new Cr;var s=GN(e),l=new GM(e,s);E(WN,l.add,l),o.add(this._axisGroup),o.add(l.getGroup()),E(HN,(function(t){e.get([t,"show"])&&UN[t](this,this.group,this._axisGroup,e)}),this),Ah(a,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)},e.prototype.remove=function(){iI(this)},e.type="singleAxis",e}(tI),UN={splitLine:function(t,e,n,i){var r=i.axis;if(!r.scale.isBlank()){var o=i.getModel("splitLine"),a=o.getModel("lineStyle"),s=a.get("color");s=s instanceof Array?s:[s];for(var l=i.coordinateSystem.getRect(),u=r.isHorizontal(),h=[],c=0,p=r.getTicksCoords({tickModel:o}),d=[],f=[],g=0;g=e.y&&t[1]<=e.y+e.height:n.contain(n.toLocalCoord(t[1]))&&t[0]>=e.y&&t[0]<=e.y+e.height},t.prototype.pointToData=function(t){var e=this.getAxis();return[e.coordToData(e.toLocalCoord(t["horizontal"===e.orient?0:1]))]},t.prototype.dataToPoint=function(t){var e=this.getAxis(),n=this.getRect(),i=[],r="horizontal"===e.orient?0:1;return t instanceof Array&&(t=t[0]),i[r]=e.toGlobalCoord(e.dataToCoord(+t)),i[1-r]=0===r?n.y+n.height/2:n.x+n.width/2,i},t.prototype.convertToPixel=function(t,e,n){return KN(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return KN(e)===this?this.pointToData(n):null},t}();function KN(t){var e=t.seriesModel,n=t.singleAxisModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}var $N={create:function(t,e){var n=[];return t.eachComponent("singleAxis",(function(i,r){var o=new qN(i,t,e);o.name="single_"+r,o.resize(i,e),i.coordinateSystem=o,n.push(o)})),t.eachSeries((function(t){if("singleAxis"===t.get("coordinateSystem")){var e=t.getReferringComponents("singleAxis",Co).models[0];t.coordinateSystem=e&&e.coordinateSystem}})),n},dimensions:jN},JN=["x","y"],QN=["width","height"],tE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.coordinateSystem,s=iE(a,1-nE(o)),l=a.dataToPoint(e)[0],u=i.get("type");if(u&&"none"!==u){var h=NR(i),c=eE[u](o,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}BR(e,t,GN(n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=GN(e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=VR(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.coordinateSystem,a=nE(r),s=iE(o,a),l=[t.x,t.y];l[a]+=e[a],l[a]=Math.min(s[1],l[a]),l[a]=Math.max(s[0],l[a]);var u=iE(o,1-a),h=(u[1]+u[0])/2,c=[h,h];return c[a]=l[a],{x:l[0],y:l[1],rotation:t.rotation,cursorPoint:c,tooltipOption:{verticalAlign:"middle"}}},e}(AR),eE={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:FR([e,n[0]],[e,n[1]],nE(t))}},shadow:function(t,e,n){var i=t.getBandWidth(),r=n[1]-n[0];return{type:"Rect",shape:GR([e-i/2,n[0]],[i,r],nE(t))}}};function nE(t){return t.isHorizontal()?0:1}function iE(t,e){var n=t.getRect();return[n[JN[e]],n[JN[e]]+n[QN[e]]]}var rE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="single",e}(gg);var oE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=Sp(e);t.prototype.init.apply(this,arguments),aE(e,r)},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),aE(this.option,e)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(Tp);function aE(t,e){var n,i=t.cellSize;1===(n=Y(i)?i:t.cellSize=[i,i]).length&&(n[1]=n[0]);var r=z([0,1],(function(t){return function(t,e){return null!=t[yp[e][0]]||null!=t[yp[e][1]]&&null!=t[yp[e][2]]}(e,t)&&(n[t]="auto"),null!=n[t]&&"auto"!==n[t]}));wp(t,e,{type:"box",ignoreSize:r})}var sE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this.group;i.removeAll();var r=t.coordinateSystem,o=r.getRangeInfo(),a=r.getOrient(),s=e.getLocaleModel();this._renderDayRect(t,o,i),this._renderLines(t,o,a,i),this._renderYearText(t,o,a,i),this._renderMonthText(t,s,a,i),this._renderWeekText(t,s,o,a,i)},e.prototype._renderDayRect=function(t,e,n){for(var i=t.coordinateSystem,r=t.getModel("itemStyle").getItemStyle(),o=i.getCellWidth(),a=i.getCellHeight(),s=e.start.time;s<=e.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,u=new Cs({shape:{x:l[0],y:l[1],width:o,height:a},cursor:"default",style:r});n.add(u)}},e.prototype._renderLines=function(t,e,n,i){var r=this,o=t.coordinateSystem,a=t.getModel(["splitLine","lineStyle"]).getLineStyle(),s=t.get(["splitLine","show"]),l=a.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=e.start,h=0;u.time<=e.end.time;h++){p(u.formatedDate),0===h&&(u=o.getDateInfo(e.start.y+"-"+e.start.m));var c=u.date;c.setMonth(c.getMonth()+1),u=o.getDateInfo(c)}function p(e){r._firstDayOfMonth.push(o.getDateInfo(e)),r._firstDayPoints.push(o.dataToRect([e],!1).tl);var l=r._getLinePointsOfOneWeek(t,e,n);r._tlpoints.push(l[0]),r._blpoints.push(l[l.length-1]),s&&r._drawSplitline(l,a,i)}p(o.getNextNDay(e.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,l,n),a,i),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,l,n),a,i)},e.prototype._getEdgesPoints=function(t,e,n){var i=[t[0].slice(),t[t.length-1].slice()],r="horizontal"===n?0:1;return i[0][r]=i[0][r]-e/2,i[1][r]=i[1][r]+e/2,i},e.prototype._drawSplitline=function(t,e,n){var i=new Ru({z2:20,shape:{points:t},style:e});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,e,n){for(var i=t.coordinateSystem,r=i.getDateInfo(e),o=[],a=0;a<7;a++){var s=i.getNextNDay(r.time,a),l=i.dataToRect([s.time],!1);o[2*s.day]=l.tl,o[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return o},e.prototype._formatterLabel=function(t,e){return X(t)&&t?(n=t,E(e,(function(t,e){n=n.replace("{"+e+"}",i?ap(t):t)})),n):U(t)?t(e):e.nameMap;var n,i},e.prototype._yearTextPositionControl=function(t,e,n,i,r){var o=e[0],a=e[1],s=["center","bottom"];"bottom"===i?(a+=r,s=["center","top"]):"left"===i?o-=r:"right"===i?(o+=r,s=["center","top"]):a-=r;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:o,y:a,style:{align:s[0],verticalAlign:s[1]}}},e.prototype._renderYearText=function(t,e,n,i){var r=t.getModel("yearLabel");if(r.get("show")){var o=r.get("margin"),a=r.get("position");a||(a="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,h="horizontal"===n?0:1,c={top:[l,s[h][1]],bottom:[l,s[1-h][1]],left:[s[1-h][0],u],right:[s[h][0],u]},p=e.start.y;+e.end.y>+e.start.y&&(p=p+"-"+e.end.y);var d=r.get("formatter"),f={start:e.start.y,end:e.end.y,nameMap:p},g=this._formatterLabel(d,f),y=new ks({z2:30,style:Uh(r,{text:g})});y.attr(this._yearTextPositionControl(y,c[a],n,a,o)),i.add(y)}},e.prototype._monthTextPositionControl=function(t,e,n,i,r){var o="left",a="top",s=t[0],l=t[1];return"horizontal"===n?(l+=r,e&&(o="center"),"start"===i&&(a="bottom")):(s+=r,e&&(a="middle"),"start"===i&&(o="right")),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderMonthText=function(t,e,n,i){var r=t.getModel("monthLabel");if(r.get("show")){var o=r.get("nameMap"),a=r.get("margin"),s=r.get("position"),l=r.get("align"),u=[this._tlpoints,this._blpoints];o&&!X(o)||(o&&(e=Mc(o)||e),o=e.get(["time","monthAbbr"])||[]);var h="start"===s?0:1,c="horizontal"===n?0:1;a="start"===s?-a:a;for(var p="center"===l,d=0;d=i.start.time&&n.timea.end.time&&t.reverse(),t},t.prototype._getRangeInfo=function(t){var e,n=[this.getDateInfo(t[0]),this.getDateInfo(t[1])];n[0].time>n[1].time&&(e=!0,n.reverse());var i=Math.floor(n[1].time/lE)-Math.floor(n[0].time/lE)+1,r=new Date(n[0].time),o=r.getDate(),a=n[1].date.getDate();r.setDate(o+i-1);var s=r.getDate();if(s!==a)for(var l=r.getTime()-n[1].time>0?1:-1;(s=r.getDate())!==a&&(r.getTime()-n[1].time)*l>0;)i-=l,r.setDate(s-l);var u=Math.floor((i+n[0].day+6)/7),h=e?1-u:u-1;return e&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:u,nthWeek:h,fweek:n[0].day,lweek:n[1].day}},t.prototype._getDateByWeeksAndDay=function(t,e,n){var i=this._getRangeInfo(n);if(t>i.weeks||0===t&&ei.lweek)return null;var r=7*(t-1)-i.fweek+e,o=new Date(i.start.time);return o.setDate(+i.start.d+r),this.getDateInfo(o)},t.create=function(e,n){var i=[];return e.eachComponent("calendar",(function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o})),e.eachSeries((function(t){"calendar"===t.get("coordinateSystem")&&(t.coordinateSystem=i[t.get("calendarIndex")||0])})),i},t.dimensions=["time","value"],t}();function hE(t){var e=t.calendarModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}function cE(t,e){var n;return E(e,(function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)})),n}var pE=["transition","enterFrom","leaveTo"],dE=pE.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function fE(t,e,n){if(n&&(!t[n]&&e[n]&&(t[n]={}),t=t[n],e=e[n]),t&&e)for(var i=n?pE:dE,r=0;r=0;l--){var p,d,f;if(f=null!=(d=xo((p=n[l]).id,null))?r.get(d):null){var g=f.parent,y=(c=vE(g),{}),v=_p(f,p,g===i?{width:o,height:a}:{width:c.width,height:c.height},null,{hv:p.hv,boundingMode:p.bounding},y);if(!vE(f).isNew&&v){for(var m=p.transition,x={},_=0;_=0)?x[b]=w:f[b]=w}rh(f,x,t,0)}else f.attr(y)}}},e.prototype._clear=function(){var t=this,e=this._elMap;e.each((function(n){bE(n,vE(n).option,e,t._lastGraphicModel)})),this._elMap=ft()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(gg);function xE(t){var e=mt(yE,t)?yE[t]:mh(t);var n=new e({});return vE(n).type=t,n}function _E(t,e,n,i){var r=xE(n);return e.add(r),i.set(t,r),vE(r).id=t,vE(r).isNew=!0,r}function bE(t,e,n,i){t&&t.parent&&("group"===t.type&&t.traverse((function(t){bE(t,e,n,i)})),BO(t,e,i),n.removeKey(vE(t).id))}function wE(t,e,n,i){t.isGroup||E([["cursor",da.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];mt(e,i)?t[i]=rt(e[i],n[1]):null==t[i]&&(t[i]=n[1])})),E(G(e),(function(n){if(0===n.indexOf("on")){var i=e[n];t[n]=U(i)?i:null}})),mt(e,"draggable")&&(t.draggable=e.draggable),null!=e.name&&(t.name=e.name),null!=e.id&&(t.id=e.id)}var SE=["x","y","radius","angle","single"],ME=["cartesian2d","polar","singleAxis"];function IE(t){return t+"Axis"}function TE(t,e){var n,i=ft(),r=[],o=ft();t.eachComponent({mainType:"dataZoom",query:e},(function(t){o.get(t.uid)||s(t)}));do{n=!1,t.eachComponent("dataZoom",a)}while(n);function a(t){!o.get(t.uid)&&function(t){var e=!1;return t.eachTargetAxis((function(t,n){var r=i.get(t);r&&r[n]&&(e=!0)})),e}(t)&&(s(t),n=!0)}function s(t){o.set(t.uid,!0),r.push(t),t.eachTargetAxis((function(t,e){(i.get(t)||i.set(t,[]))[e]=!0}))}return r}function CE(t){var e=t.ecModel,n={infoList:[],infoMap:ft()};return t.eachTargetAxis((function(t,i){var r=e.getComponent(IE(t),i);if(r){var o=r.getCoordSysModel();if(o){var a=o.uid,s=n.infoMap.get(a);s||(s={model:o,axisModels:[]},n.infoList.push(s),n.infoMap.set(a,s)),s.axisModels.push(r)}}})),n}var DE=function(){function t(){this.indexList=[],this.indexMap=[]}return t.prototype.add=function(t){this.indexMap[t]||(this.indexList.push(t),this.indexMap[t]=!0)},t}(),AE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return n(e,t),e.prototype.init=function(t,e,n){var i=kE(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var e=kE(t);C(this.option,t,!0),C(this.settledOption,e,!0),this._doInit(e)},e.prototype._doInit=function(t){var e=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(t,i){"value"===this._rangePropMode[i]&&(e[t[0]]=n[t[0]]=null)}),this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),e=this._targetAxisInfoMap=ft();this._fillSpecifiedTargetAxis(e)?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(e,this._orient)),this._noTarget=!0,e.each((function(t){t.indexList.length&&(this._noTarget=!1)}),this)},e.prototype._fillSpecifiedTargetAxis=function(t){var e=!1;return E(SE,(function(n){var i=this.getReferringComponents(IE(n),Do);if(i.specified){e=!0;var r=new DE;E(i.models,(function(t){r.add(t.componentIndex)})),t.set(n,r)}}),this),e},e.prototype._fillAutoTargetAxisByOrient=function(t,e){var n=this.ecModel,i=!0;if(i){var r="vertical"===e?"y":"x";o(n.findComponents({mainType:r+"Axis"}),r)}i&&o(n.findComponents({mainType:"singleAxis",filter:function(t){return t.get("orient",!0)===e}}),"single");function o(e,n){var r=e[0];if(r){var o=new DE;if(o.add(r.componentIndex),t.set(n,o),i=!1,"x"===n||"y"===n){var a=r.getReferringComponents("grid",Co).models[0];a&&E(e,(function(t){r.componentIndex!==t.componentIndex&&a===t.getReferringComponents("grid",Co).models[0]&&o.add(t.componentIndex)}))}}}i&&E(SE,(function(e){if(i){var r=n.findComponents({mainType:IE(e),filter:function(t){return"category"===t.get("type",!0)}});if(r[0]){var o=new DE;o.add(r[0].componentIndex),t.set(e,o),i=!1}}}),this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis((function(e){!t&&(t=e)}),this),"y"===t?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var e=this._rangePropMode,n=this.get("rangeMode");E([["start","startValue"],["end","endValue"]],(function(i,r){var o=null!=t[i[0]],a=null!=t[i[1]];o&&!a?e[r]="percent":!o&&a?e[r]="value":n?e[r]=n[r]:o&&(e[r]="percent")}))},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis((function(e,n){null==t&&(t=this.ecModel.getComponent(IE(e),n))}),this),t},e.prototype.eachTargetAxis=function(t,e){this._targetAxisInfoMap.each((function(n,i){E(n.indexList,(function(n){t.call(e,i,n)}))}))},e.prototype.getAxisProxy=function(t,e){var n=this.getAxisModel(t,e);if(n)return n.__dzAxisProxy},e.prototype.getAxisModel=function(t,e){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[e])return this.ecModel.getComponent(IE(t),e)},e.prototype.setRawRange=function(t){var e=this.option,n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(i){null==t[i[0]]&&null==t[i[1]]||(e[i[0]]=n[i[0]]=t[i[0]],e[i[1]]=n[i[1]]=t[i[1]])}),this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var e=this.option;E(["start","startValue","end","endValue"],(function(n){e[n]=t[n]}))},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},e.prototype.getValueRange=function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return t.__dzAxisProxy;for(var e,n=this._targetAxisInfoMap.keys(),i=0;i=0}(e)){var n=IE(this._dimName),i=e.getReferringComponents(n,Co).models[0];i&&this._axisIndex===i.componentIndex&&t.push(e)}}),this),t},t.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},t.prototype.getMinMaxSpan=function(){return T(this._minMaxSpan)},t.prototype.calculateDataWindow=function(t){var e,n=this._dataExtent,i=this.getAxisModel().axis.scale,r=this._dataZoomModel.getRangePropMode(),o=[0,100],a=[],s=[];RE(["start","end"],(function(l,u){var h=t[l],c=t[l+"Value"];"percent"===r[u]?(null==h&&(h=o[u]),c=i.parse(Nr(h,o,n))):(e=!0,h=Nr(c=null==c?n[u]:i.parse(c),n,o)),s[u]=c,a[u]=h})),NE(s),NE(a);var l=this._minMaxSpan;function u(t,e,n,r,o){var a=o?"Span":"ValueSpan";lk(0,t,n,"all",l["min"+a],l["max"+a]);for(var s=0;s<2;s++)e[s]=Nr(t[s],n,r,!0),o&&(e[s]=i.parse(e[s]))}return e?u(s,a,n,o,!1):u(a,s,o,n,!0),{valueWindow:s,percentWindow:a}},t.prototype.reset=function(t){if(t===this._dataZoomModel){var e=this.getTargetSeriesModels();this._dataExtent=function(t,e,n){var i=[1/0,-1/0];RE(n,(function(t){!function(t,e,n){e&&E(c_(e,n),(function(n){var i=e.getApproximateExtent(n);i[0]t[1]&&(t[1]=i[1])}))}(i,t.getData(),e)}));var r=t.getAxisModel(),o=e_(r.axis.scale,r,i).calculate();return[o.min,o.max]}(this,this._dimName,e),this._updateMinMaxSpan();var n=this.calculateDataWindow(t.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},t.prototype.filterData=function(t,e){if(t===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),r=t.get("filterMode"),o=this._valueWindow;"none"!==r&&RE(i,(function(t){var e=t.getData(),i=e.mapDimensionsAll(n);if(i.length){if("weakFilter"===r){var a=e.getStore(),s=z(i,(function(t){return e.getDimensionIndex(t)}),e);e.filterSelf((function(t){for(var e,n,r,l=0;lo[1];if(h&&!c&&!p)return!0;h&&(r=!0),c&&(e=!0),p&&(n=!0)}return r&&e&&n}))}else RE(i,(function(n){if("empty"===r)t.setData(e=e.map(n,(function(t){return function(t){return t>=o[0]&&t<=o[1]}(t)?t:NaN})));else{var i={};i[n]=o,e.selectRange(i)}}));RE(i,(function(t){e.setApproximateExtent(o,t)}))}}))}},t.prototype._updateMinMaxSpan=function(){var t=this._minMaxSpan={},e=this._dataZoomModel,n=this._dataExtent;RE(["min","max"],(function(i){var r=e.get(i+"Span"),o=e.get(i+"ValueSpan");null!=o&&(o=this.getAxisModel().axis.scale.parse(o)),null!=o?r=Nr(n[0]+o,n,[0,100],!0):null!=r&&(o=Nr(r,[0,100],n,!0)-n[0]),t[i+"Span"]=r,t[i+"ValueSpan"]=o}),this)},t.prototype._setAxisModel=function(){var t=this.getAxisModel(),e=this._percentWindow,n=this._valueWindow;if(e){var i=Gr(n,[0,500]);i=Math.min(i,20);var r=t.axis.scale.rawExtentInfo;0!==e[0]&&r.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==e[1]&&r.setDeterminedMinMax("max",+n[1].toFixed(i)),r.freeze()}},t}();var zE={getTargetSeries:function(t){function e(e){t.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,r){var o=t.getComponent(IE(i),r);e(i,r,o,n)}))}))}e((function(t,e,n,i){n.__dzAxisProxy=null}));var n=[];e((function(e,i,r,o){r.__dzAxisProxy||(r.__dzAxisProxy=new EE(e,i,o,t),n.push(r.__dzAxisProxy))}));var i=ft();return E(n,(function(t){E(t.getTargetSeriesModels(),(function(t){i.set(t.uid,t)}))})),i},overallReset:function(t,e){t.eachComponent("dataZoom",(function(t){t.eachTargetAxis((function(e,n){t.getAxisProxy(e,n).reset(t)})),t.eachTargetAxis((function(n,i){t.getAxisProxy(n,i).filterData(t,e)}))})),t.eachComponent("dataZoom",(function(t){var e=t.findRepresentativeAxisProxy();if(e){var n=e.getDataPercentWindow(),i=e.getDataValueWindow();t.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var VE=!1;function BE(t){VE||(VE=!0,t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,zE),function(t){t.registerAction("dataZoom",(function(t,e){E(TE(e,t),(function(e){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})}))}))}(t),t.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}function FE(t){t.registerComponentModel(LE),t.registerComponentView(OE),BE(t)}var GE=function(){},WE={};function HE(t,e){WE[t]=e}function YE(t){return WE[t]}var UE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){t.prototype.optionUpdated.apply(this,arguments);var e=this.ecModel;E(this.option.feature,(function(t,n){var i=YE(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(e)),C(t,i.defaultOption))}))},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}},e}(Tp);function XE(t,e){var n=ip(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),t=new Cs({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}var ZE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this.group;if(r.removeAll(),t.get("show")){var o=+t.get("itemSize"),a="vertical"===t.get("orient"),s=t.get("feature")||{},l=this._features||(this._features={}),u=[];E(s,(function(t,e){u.push(e)})),new Im(this._featureNames||[],u).add(h).update(h).remove(H(h,null)).execute(),this._featureNames=u,function(t,e,n){var i=e.getBoxLayoutParams(),r=e.get("padding"),o={width:n.getWidth(),height:n.getHeight()},a=xp(i,o,r);mp(e.get("orient"),t,e.get("itemGap"),a.width,a.height),_p(t,i,o,r)}(r,t,n),r.add(XE(r.getBoundingRect(),t)),a||r.eachChild((function(t){var e=t.__title,i=t.ensureState("emphasis"),a=i.textConfig||(i.textConfig={}),s=t.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!U(l)&&e){var u=l.style||(l.style={}),h=cr(e,ks.makeFont(u)),c=t.x+r.x,p=!1;t.y+r.y+o+h.height>n.getHeight()&&(a.position="top",p=!0);var d=p?-5-h.height:o+10;c+h.width/2>n.getWidth()?(a.position=["100%",d],u.align="right"):c-h.width/2<0&&(a.position=[0,d],u.align="left")}}))}function h(h,c){var p,d=u[h],f=u[c],g=s[d],y=new dc(g,t,t.ecModel);if(i&&null!=i.newTitle&&i.featureName===d&&(g.title=i.newTitle),d&&!f){if(function(t){return 0===t.indexOf("my")}(d))p={onclick:y.option.onclick,featureName:d};else{var v=YE(d);if(!v)return;p=new v}l[d]=p}else if(!(p=l[f]))return;p.uid=gc("toolbox-feature"),p.model=y,p.ecModel=e,p.api=n;var m=p instanceof GE;d||!f?!y.get("show")||m&&p.unusable?m&&p.remove&&p.remove(e,n):(!function(i,s,l){var u,h,c=i.getModel("iconStyle"),p=i.getModel(["emphasis","iconStyle"]),d=s instanceof GE&&s.getIcons?s.getIcons():i.get("icon"),f=i.get("title")||{};X(d)?(u={})[l]=d:u=d;X(f)?(h={})[l]=f:h=f;var g=i.iconPaths={};E(u,(function(l,u){var d=Ph(l,{},{x:-o/2,y:-o/2,width:o,height:o});d.setStyle(c.getItemStyle()),d.ensureState("emphasis").style=p.getItemStyle();var f=new ks({style:{text:h[u],align:p.get("textAlign"),borderRadius:p.get("textBorderRadius"),padding:p.get("textPadding"),fill:null},ignore:!0});d.setTextContent(f),Eh({el:d,componentModel:t,itemName:u,formatterParamsExtra:{title:h[u]}}),d.__title=h[u],d.on("mouseover",(function(){var e=p.getItemStyle(),i=a?null==t.get("right")&&"right"!==t.get("left")?"right":"left":null==t.get("bottom")&&"bottom"!==t.get("top")?"bottom":"top";f.setStyle({fill:p.get("textFill")||e.fill||e.stroke||"#000",backgroundColor:p.get("textBackgroundColor")}),d.setTextConfig({position:p.get("textPosition")||i}),f.ignore=!t.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",u])&&n.leaveEmphasis(this),f.hide()})),("emphasis"===i.get(["iconStatus",u])?_l:bl)(d),r.add(d),d.on("click",W(s.onclick,s,e,n,u)),g[u]=d}))}(y,p,d),y.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&("emphasis"===e?_l:bl)(i[t])},p instanceof GE&&p.render&&p.render(y,e,n,i)):m&&p.dispose&&p.dispose(e,n)}},e.prototype.updateView=function(t,e,n,i){E(this._features,(function(t){t instanceof GE&&t.updateView&&t.updateView(t.model,e,n,i)}))},e.prototype.remove=function(t,e){E(this._features,(function(n){n instanceof GE&&n.remove&&n.remove(t,e)})),this.group.removeAll()},e.prototype.dispose=function(t,e){E(this._features,(function(n){n instanceof GE&&n.dispose&&n.dispose(t,e)}))},e.type="toolbox",e}(gg);var jE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o="svg"===e.getZr().painter.getType(),a=o?"svg":n.get("type",!0)||"png",s=e.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),l=r.browser;if(U(MouseEvent)&&(l.newEdge||!l.ie&&!l.edge)){var u=document.createElement("a");u.download=i+"."+a,u.target="_blank",u.href=s;var h=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});u.dispatchEvent(h)}else if(window.navigator.msSaveOrOpenBlob||o){var c=s.split(","),p=c[0].indexOf("base64")>-1,d=o?decodeURIComponent(c[1]):c[1];p&&(d=window.atob(d));var f=i+"."+a;if(window.navigator.msSaveOrOpenBlob){for(var g=d.length,y=new Uint8Array(g);g--;)y[g]=d.charCodeAt(g);var v=new Blob([y]);window.navigator.msSaveOrOpenBlob(v,f)}else{var m=document.createElement("iframe");document.body.appendChild(m);var x=m.contentWindow,_=x.document;_.open("image/svg+xml","replace"),_.write(d),_.close(),x.focus(),_.execCommand("SaveAs",!0,f),document.body.removeChild(m)}}else{var b=n.get("lang"),w='',S=window.open();S.document.write(w),S.document.title=i}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])}},e}(GE),qE="__ec_magicType_stack__",KE=[["line","bar"],["stack"]],$E=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return E(t.get("type"),(function(t){e[t]&&(n[t]=e[t])})),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(JE[n]){var o,a={series:[]};E(KE,(function(t){P(t,n)>=0&&E(t,(function(t){i.setIconStatus(t,"normal")}))})),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},(function(t){var e=t.subType,r=t.id,o=JE[n](e,r,t,i);o&&(k(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",h=t.getReferringComponents(u,Co).models[0].componentIndex;a[u]=a[u]||[];for(var c=0;c<=h;c++)a[u][h]=a[u][h]||{};a[u][h].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(o=C({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(GE),JE={line:function(t,e,n,i){if("bar"===t)return C({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return C({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===qE;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),C({id:e,stack:r?"":qE},i.get(["option","stack"])||{},!0)}};cm({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(t,e){e.mergeOption(t.newOption)}));var QE=new Array(60).join("-"),tz="\t";function ez(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var nz=new RegExp("[\t]+","g");function iz(t,e){var n=t.split(new RegExp("\n*"+QE+"\n*","g")),i={series:[]};return E(n,(function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(tz)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=z(ez(e.shift()).split(nz),(function(t){return{name:t,data:[]}})),r=0;r=0)&&t(r,i._targetInfoList)}))}return t.prototype.setOutputRanges=function(t,e){return this.matchOutputRanges(t,e,(function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=vz[t.brushType](0,n,e);t.__rangeOffset={offset:xz[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}})),t},t.prototype.matchOutputRanges=function(t,e,n){E(t,(function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&E(i.coordSyses,(function(i){var r=vz[t.brushType](1,i,t.range,!0);n(t,r.values,i,e)}))}),this)},t.prototype.setInputRanges=function(t,e){E(t,(function(t){var n,i,r,o,a,s=this.findTargetInfo(t,e);if(t.range=t.range||[],s&&!0!==s){t.panelId=s.panelId;var l=vz[t.brushType](0,s.coordSys,t.coordRange),u=t.__rangeOffset;t.range=u?xz[t.brushType](l.values,u.offset,(n=l.xyMinMax,i=u.xyMinMax,r=bz(n),o=bz(i),a=[r[0]/o[0],r[1]/o[1]],isNaN(a[0])&&(a[0]=1),isNaN(a[1])&&(a[1]=1),a)):l.values}}),this)},t.prototype.makePanelOpts=function(t,e){return z(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e?e(n):null,clipPath:hL(i),isTargetByCursor:pL(i,t,n.coordSysModel),getLinearBrushOtherExtent:cL(i)}}))},t.prototype.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&P(i.coordSyses,e.coordinateSystem)>=0},t.prototype.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=dz(e,t),r=0;rt[1]&&t.reverse(),t}function dz(t,e){return Io(t,e,{includeMainTypes:hz})}var fz={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=ft(),a={},s={};(n||i||r)&&(E(n,(function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0})),E(i,(function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0})),E(r,(function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0})),o.each((function(t){var r=t.coordinateSystem,o=[];E(r.getCartesians(),(function(t,e){(P(n,t.getAxis("x").model)>=0||P(i,t.getAxis("y").model)>=0)&&o.push(t)})),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:yz.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})})))},geo:function(t,e){E(t.geoModels,(function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:yz.geo})}))}},gz=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],yz={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var t=this.coordSys,e=t.getBoundingRect().clone();return e.applyTransform(Ih(t)),e}},vz={lineX:H(mz,0),lineY:H(mz,1),rect:function(t,e,n,i){var r=t?e.pointToData([n[0][0],n[1][0]],i):e.dataToPoint([n[0][0],n[1][0]],i),o=t?e.pointToData([n[0][1],n[1][1]],i):e.dataToPoint([n[0][1],n[1][1]],i),a=[pz([r[0],o[0]]),pz([r[1],o[1]])];return{values:a,xyMinMax:a}},polygon:function(t,e,n,i){var r=[[1/0,-1/0],[1/0,-1/0]];return{values:z(n,(function(n){var o=t?e.pointToData(n,i):e.dataToPoint(n,i);return r[0][0]=Math.min(r[0][0],o[0]),r[1][0]=Math.min(r[1][0],o[1]),r[0][1]=Math.max(r[0][1],o[0]),r[1][1]=Math.max(r[1][1],o[1]),o})),xyMinMax:r}}};function mz(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=pz(z([0,1],(function(t){return e?r.coordToData(r.toLocalCoord(i[t]),!0):r.toGlobalCoord(r.dataToCoord(i[t]))}))),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}var xz={lineX:H(_z,0),lineY:H(_z,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return z(t,(function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]}))}};function _z(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function bz(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}var wz,Sz,Mz=E,Iz=uo+"toolbox-dataZoom_",Tz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){this._brushController||(this._brushController=new Ok(n.getZr()),this._brushController.on("brush",W(this._onBrush,this)).mount()),function(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new cz(Dz(t),e,{include:["grid"]}).makePanelOpts(r,(function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(a).enableBrush(!(!o||!a.length)&&{brushType:"auto",brushStyle:t.getModel("brushStyle").getItemStyle()})}(t,e,this,i,n),function(t,e){t.setIconStatus("back",function(t){return lz(t).length}(e)>1?"emphasis":"normal")}(t,e)},e.prototype.onclick=function(t,e,n){Cz[n].call(this)},e.prototype.remove=function(t,e){this._brushController&&this._brushController.unmount()},e.prototype.dispose=function(t,e){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var e=t.areas;if(t.isEnd&&e.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new cz(Dz(this.model),i,{include:["grid"]}).matchOutputRanges(e,i,(function(t,e,n){if("cartesian2d"===n.type){var i=t.brushType;"rect"===i?(r("x",n,e[0]),r("y",n,e[1])):r({lineX:"x",lineY:"y"}[i],n,e)}})),function(t,e){var n=lz(t);az(e,(function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}})),n.push(e)}(i,n),this._dispatchZoomAction(n)}function r(t,e,r){var o=e.getAxis(t),a=o.model,s=function(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)})),i}(t,a,i),l=s.findRepresentativeAxisProxy(a).getMinMaxSpan();null==l.minValueSpan&&null==l.maxValueSpan||(r=lk(0,r.slice(),o.scale.getExtent(),0,l.minValueSpan,l.maxValueSpan)),s&&(n[s.id]={dataZoomId:s.id,startValue:r[0],endValue:r[1]})}},e.prototype._dispatchZoomAction=function(t){var e=[];Mz(t,(function(t,n){e.push(T(t))})),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},e.getDefaultOption=function(t){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}},e}(GE),Cz={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(function(t){var e=lz(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return az(n,(function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}})),i}(this.ecModel))}};function Dz(t){var e={xAxisIndex:t.get("xAxisIndex",!0),yAxisIndex:t.get("yAxisIndex",!0),xAxisId:t.get("xAxisId",!0),yAxisId:t.get("yAxisId",!0)};return null==e.xAxisIndex&&null==e.xAxisId&&(e.xAxisIndex="all"),null==e.yAxisIndex&&null==e.yAxisId&&(e.yAxisIndex="all"),e}wz="dataZoom",Sz=function(t){var e=t.getComponent("toolbox",0),n=["feature","dataZoom"];if(e&&null!=e.get(n)){var i=e.getModel(n),r=[],o=Io(t,Dz(i));return Mz(o.xAxisModels,(function(t){return a(t,"xAxis","xAxisIndex")})),Mz(o.yAxisModels,(function(t){return a(t,"yAxis","yAxisIndex")})),r}function a(t,e,n){var o=t.componentIndex,a={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:Iz+e+o};a[n]=o,r.push(a)}},lt(null==jp.get(wz)&&Sz),jp.set(wz,Sz);var Az=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},e}(Tp);function kz(t){var e=t.get("confine");return null!=e?!!e:"richText"===t.get("renderMode")}function Lz(t){if(r.domSupported)for(var e=document.documentElement.style,n=0,i=t.length;n-1?(u+="top:50%",h+="translateY(-50%) rotate("+(a="left"===s?-225:-45)+"deg)"):(u+="left:50%",h+="translateX(-50%) rotate("+(a="top"===s?225:45)+"deg)");var c=a*Math.PI/180,p=l+r,d=p*Math.abs(Math.cos(c))+p*Math.abs(Math.sin(c)),f=e+" solid "+r+"px;";return'
'}(n,i,r)),X(t))o.innerHTML=t+a;else if(t){o.innerHTML="",Y(t)||(t=[t]);for(var s=0;s=0?this._tryShow(n,i):"leave"===e&&this._hide(i))}),this))},e.prototype._keepShow=function(){var t=this._tooltipModel,e=this._ecModel,n=this._api,i=t.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var r=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&r.manuallyShowTip(t,e,n,{x:r._lastX,y:r._lastY,dataByCoordSys:r._lastDataByCoordSys})}))}},e.prototype.manuallyShowTip=function(t,e,n,i){if(i.from!==this.uid&&!r.node&&n.getDom()){var o=jz(i,n);this._ticket="";var a=i.dataByCoordSys,s=function(t,e,n){var i=To(t).queryOptionMap,r=i.keys()[0];if(!r||"series"===r)return;var o,a=Ao(e,r,i.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}).models[0];if(!a)return;if(n.getViewOfComponentModel(a).group.traverse((function(e){var n=Hs(e).tooltipConfig;if(n&&n.name===t.name)return o=e,!0})),o)return{componentMainType:r,componentIndex:a.componentIndex,el:o}}(i,e,n);if(s){var l=s.el.getBoundingRect().clone();l.applyTransform(s.el.transform),this._tryShow({offsetX:l.x+l.width/2,offsetY:l.y+l.height/2,target:s.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&null!=i.x&&null!=i.y){var u=Uz;u.x=i.x,u.y=i.y,u.update(),Hs(u).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:u},o)}else if(a)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:a,tooltipOption:i.tooltipOption},o);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var h=eN(i,e),c=h.point[0],p=h.point[1];null!=c&&null!=p&&this._tryShow({offsetX:c,offsetY:p,target:h.el,position:i.position,positionDefault:"bottom"},o)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,e,n,i){var r=this._tooltipContent;!this._alwaysShowContent&&this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(jz(i,n))},e.prototype._manuallyAxisShowTip=function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s)if("axis"===Zz([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},e.prototype._tryShow=function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,t);else if(n){var r,o;this._lastDataByCoordSys=null,xy(n,(function(t){return null!=Hs(t).dataIndex?(r=t,!0):null!=Hs(t).tooltipConfig?(o=t,!0):void 0}),!0),r?this._showSeriesItemTooltip(t,r,e):o?this._showComponentItemTooltip(t,o,e):this._hide(e)}else this._lastDataByCoordSys=null,this._hide(e)}},e.prototype._showOrMove=function(t,e){var n=t.get("showDelay");e=W(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},e.prototype._showAxisTooltip=function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=Zz([e.tooltipOption],i),a=this._renderMode,s=[],l=Xf("section",{blocks:[],noHeader:!0}),u=[],h=new ig;E(t,(function(t){E(t.dataByAxis,(function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),r=t.value;if(e&&null!=r){var o=zR(r,e.axis,n,t.seriesDataIndices,t.valueLabelOpt),c=Xf("section",{header:o,noHeader:!ut(o),sortBlocks:!0,blocks:[]});l.blocks.push(c),E(t.seriesDataIndices,(function(l){var p=n.getSeriesByIndex(l.seriesIndex),d=l.dataIndexInside,f=p.getDataParams(d);if(!(f.dataIndex<0)){f.axisDim=t.axisDim,f.axisIndex=t.axisIndex,f.axisType=t.axisType,f.axisId=t.axisId,f.axisValue=s_(e.axis,{value:r}),f.axisValueLabel=o,f.marker=h.makeTooltipMarker("item",pp(f.color),a);var g=uf(p.formatTooltip(d,!0,null)),y=g.frag;if(y){var v=Zz([p],i).get("valueFormatter");c.blocks.push(v?A({valueFormatter:v},y):y)}g.text&&u.push(g.text),s.push(f)}}))}}))})),l.blocks.reverse(),u.reverse();var c=e.position,p=o.get("order"),d=Jf(l,h,a,p,n.get("useUTC"),o.get("textStyle"));d&&u.unshift(d);var f="richText"===a?"\n\n":"
",g=u.join(f);this._showOrMove(o,(function(){this._updateContentNotChangedOnAxis(t,s)?this._updatePosition(o,c,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],c,null,h)}))},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=Hs(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,h=s.getData(u),c=this._renderMode,p=t.positionDefault,d=Zz([h.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),f=d.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),y=new ig;g.marker=y.makeTooltipMarker("item",pp(g.color),c);var v=uf(s.formatTooltip(l,!1,u)),m=d.get("order"),x=d.get("valueFormatter"),_=v.frag,b=_?Jf(x?A({valueFormatter:x},_):_,y,c,m,i.get("useUTC"),d.get("textStyle")):v.text,w="item_"+s.name+"_"+l;this._showOrMove(d,(function(){this._showTooltipContent(d,b,g,w,t.offsetX,t.offsetY,t.position,t.target,y)})),n({type:"showTip",dataIndexInside:l,dataIndex:h.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i=Hs(e),r=i.tooltipConfig.option||{};if(X(r)){r={content:r,formatter:r}}var o=[r],a=this._ecModel.getComponent(i.componentMainType,i.componentIndex);a&&o.push(a),o.push({formatter:r.content});var s=t.positionDefault,l=Zz(o,this._tooltipModel,s?{position:s}:null),u=l.get("content"),h=Math.random()+"",c=new ig;this._showOrMove(l,(function(){var n=T(l.get("formatterParams")||{});this._showTooltipContent(l,u,n,h,t.offsetX,t.offsetY,t.position,e,c)})),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent;u.setEnterable(t.get("enterable"));var h=t.get("formatter");a=a||t.get("position");var c=e,p=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor")).color;if(h)if(X(h)){var d=t.ecModel.get("useUTC"),f=Y(n)?n[0]:n;c=h,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(c=Vc(f.axisValue,c,d)),c=hp(c,n,!0)}else if(U(h)){var g=W((function(e,i){e===this._ticket&&(u.setContent(i,l,t,p,a),this._updatePosition(t,a,r,o,u,n,s))}),this);this._ticket=i,c=h(n,i,g)}else c=h;u.setContent(c,l,t,p,a),u.show(t,p),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i){return"axis"===n||Y(e)?{color:i||("html"===this._renderMode?"#fff":"none")}:Y(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),p=a&&a.getBoundingRect().clone();if(a&&p.applyTransform(a.transform),U(e)&&(e=e([n,i],o,r.el,p,{viewSize:[s,l],contentSize:u.slice()})),Y(e))n=Er(e[0],s),i=Er(e[1],l);else if(q(e)){var d=e;d.width=u[0],d.height=u[1];var f=xp(d,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else if(X(e)&&a){var g=function(t,e,n,i){var r=n[0],o=n[1],a=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,u=e.width,h=e.height;switch(t){case"inside":s=e.x+u/2-r/2,l=e.y+h/2-o/2;break;case"top":s=e.x+u/2-r/2,l=e.y-o-a;break;case"bottom":s=e.x+u/2-r/2,l=e.y+h+a;break;case"left":s=e.x-r-a,l=e.y+h/2-o/2;break;case"right":s=e.x+u+a,l=e.y+h/2-o/2}return[s,l]}(e,p,u,t.get("borderWidth"));n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getSize(),l=s[0],u=s[1];null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,h?null:20,c?null:20);n=g[0],i=g[1]}if(h&&(n-=qz(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=qz(c)?u[1]/2:"bottom"===c?u[1]:0),kz(t)){g=function(t,e,n,i,r){var o=n.getSize(),a=o[0],s=o[1];return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,e){var n=this._lastDataByCoordSys,i=this._cbParamsList,r=!!n&&n.length===t.length;return r&&E(n,(function(n,o){var a=n.dataByAxis||[],s=(t[o]||{}).dataByAxis||[];(r=r&&a.length===s.length)&&E(a,(function(t,n){var o=s[n]||{},a=t.seriesDataIndices||[],l=o.seriesDataIndices||[];(r=r&&t.value===o.value&&t.axisType===o.axisType&&t.axisId===o.axisId&&a.length===l.length)&&E(a,(function(t,e){var n=l[e];r=r&&t.seriesIndex===n.seriesIndex&&t.dataIndex===n.dataIndex})),i&&E(t.seriesDataIndices,(function(t){var n=t.seriesIndex,o=e[n],a=i[n];o&&a&&a.data!==o.data&&(r=!1)}))}))})),this._lastDataByCoordSys=t,this._cbParamsList=e,!!r},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){!r.node&&e.getDom()&&(kg(this,"_updatePosition"),this._tooltipContent.dispose(),QR("itemTooltip",e))},e.type="tooltip",e}(gg);function Zz(t,e,n){var i,r=e.ecModel;n?(i=new dc(n,r,r),i=new dc(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof dc&&(a=a.get("tooltip",!0)),X(a)&&(a={formatter:a}),a&&(i=new dc(a,i,r)))}return i}function jz(t,e){return t.dispatchAction||W(e.dispatchAction,e)}function qz(t){return"center"===t||"middle"===t}var Kz=["rect","polygon","keep","clear"];function $z(t,e){var n=ho(t?t.brush:[]);if(n.length){var i=[];E(n,(function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))}));var r=t&&t.toolbox;Y(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),function(t){var e={};E(t,(function(t){e[t]=1})),t.length=0,E(e,(function(e,n){t.push(n)}))}(s),e&&!s.length&&s.push.apply(s,Kz)}}var Jz=E;function Qz(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function tV(t,e,n){var i={};return Jz(e,(function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);Jz(t[e],(function(t,i){if(iD.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new iD(r),"opacity"===i&&((r=T(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new iD(r))}}))})),i}function eV(t,e,n){var i;E(n,(function(t){e.hasOwnProperty(t)&&Qz(e[t])&&(i=!0)})),i&&E(n,(function(n){e.hasOwnProperty(n)&&Qz(e[n])?t[n]=T(e[n]):delete t[n]}))}var nV={lineX:iV(0),lineY:iV(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&y_(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(y_(i,r,o)||y_(i,r+a,o)||y_(i,r,o+s)||y_(i,r+a,o+s)||sr.create(t).contain(l[0],l[1])||Oh(r,o,r+a,o,i)||Oh(r,o,r,o+s,i)||Oh(r+a,o,r+a,o+s,i)||Oh(r,o+s,r+a,o+s,i))||void 0}}};function iV(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return rV(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]e[1][1]&&(e[1][1]=o[1])}return e&&dV(e)}};function dV(t){return new sr(t[0][0],t[1][0],t[0][1]-t[0][0],t[1][1]-t[1][0])}var fV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e,this.model,(this._brushController=new Ok(e.getZr())).on("brush",W(this._onBrush,this)).mount()},e.prototype.render=function(t,e,n,i){this.model=t,this._updateController(t,e,n,i)},e.prototype.updateTransform=function(t,e,n,i){lV(e),this._updateController(t,e,n,i)},e.prototype.updateVisual=function(t,e,n,i){this.updateTransform(t,e,n,i)},e.prototype.updateView=function(t,e,n,i){this._updateController(t,e,n,i)},e.prototype._updateController=function(t,e,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var e=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:e,areas:T(n),$from:e}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:e,areas:T(n),$from:e})},e.type="brush",e}(gg),gV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.areas=[],n.brushOption={},n}return n(e,t),e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&eV(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:"#ddd"},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=z(t,(function(t){return yV(this.option,t)}),this))},e.prototype.setBrushOption=function(t){this.brushOption=yV(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(210,219,238,0.3)",borderColor:"#D2DBEE"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},e}(Tp);function yV(t,e){return C({brushType:t.brushType,brushMode:t.brushMode,transformable:t.transformable,brushStyle:new dc(t.brushStyle).getItemStyle(),removeOnClick:t.removeOnClick,z:t.z},e,!0)}var vV=["rect","polygon","lineX","lineY","keep","clear"],mV=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n){var i,r,o;e.eachComponent({mainType:"brush"},(function(t){i=t.brushType,r=t.brushOption.brushMode||"single",o=o||!!t.areas.length})),this._brushType=i,this._brushMode=r,E(t.get("type",!0),(function(e){t.setIconStatus(e,("keep"===e?"multiple"===r:"clear"===e?o:e===i)?"emphasis":"normal")}))},e.prototype.updateView=function(t,e,n){this.render(t,e,n)},e.prototype.getIcons=function(){var t=this.model,e=t.get("icon",!0),n={};return E(t.get("type",!0),(function(t){e[t]&&(n[t]=e[t])})),n},e.prototype.onclick=function(t,e,n){var i=this._brushType,r=this._brushMode;"clear"===n?(e.dispatchAction({type:"axisAreaSelect",intervals:[]}),e.dispatchAction({type:"brush",command:"clear",areas:[]})):e.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?i:i!==n&&n,brushMode:"keep"===n?"multiple"===r?"single":"multiple":r}})},e.getDefaultOption=function(t){return{show:!0,type:vV.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])}},e}(GE);var xV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode={type:"box",ignoreSize:!0},n}return n(e,t),e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:"#464646"},subtextStyle:{fontSize:12,color:"#6E7079"}},e}(Tp),_V=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=rt(t.get("textBaseline"),t.get("textVerticalAlign")),l=new ks({style:Uh(r,{text:t.get("text"),fill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),h=t.get("subtext"),c=new ks({style:Uh(o,{text:h,fill:o.getTextColor(),y:u.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),p=t.get("link"),d=t.get("sublink"),f=t.get("triggerEvent",!0);l.silent=!p&&!f,c.silent=!d&&!f,p&&l.on("click",(function(){dp(p,"_"+t.get("target"))})),d&&c.on("click",(function(){dp(d,"_"+t.get("subtarget"))})),Hs(l).eventData=Hs(c).eventData=f?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(l),h&&i.add(c);var g=i.getBoundingRect(),y=t.getBoxLayoutParams();y.width=g.width,y.height=g.height;var v=xp(y,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?v.x+=v.width:"center"===a&&(v.x+=v.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?v.y+=v.height:"middle"===s&&(v.y+=v.height/2),s=s||"top"),i.x=v.x,i.y=v.y,i.markRedraw();var m={align:a,verticalAlign:s};l.setStyle(m),c.setStyle(m),g=i.getBoundingRect();var x=v.margin,_=t.getItemStyle(["color","opacity"]);_.fill=t.get("backgroundColor");var b=new Cs({shape:{x:g.x-x[3],y:g.y-x[0],width:g.width+x[1]+x[3],height:g.height+x[0]+x[2],r:t.get("borderRadius")},style:_,subPixelOptimize:!0,silent:!0});i.add(b)}},e.type="title",e}(gg);var bV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode="box",n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){null==t&&(t=this.option.currentIndex);var e=this._data.count();this.option.loop?t=(t%e+e)%e:(t>=e&&(t=e-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t,e=this.option,n=e.data||[],i=e.axisType,r=this._names=[];"category"===i?(t=[],E(n,(function(e,n){var i,o=xo(fo(e),"");q(e)?(i=T(e)).value=n:i=n,t.push(i),r.push(o)}))):t=n;var o={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new qm([{name:"value",type:o}],this)).initData(t,r)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},e}(Tp),wV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline.slider",e.defaultOption=yc(bV.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),e}(bV);R(wV,lf.prototype);var SV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline",e}(gg),MV=function(t){function e(e,n,i,r){var o=t.call(this,e,n,i)||this;return o.type=r||"value",o}return n(e,t),e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},e}(H_),IV=Math.PI,TV=So(),CV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.api=e},e.prototype.render=function(t,e,n){if(this.model=t,this.api=n,this.ecModel=e,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),r=this._createGroup("_mainGroup"),o=this._createGroup("_labelGroup"),a=this._axis=this._createAxis(i,t);t.formatTooltip=function(t){return Xf("nameValue",{noName:!0,value:a.scale.getLabel({value:t})})},E(["AxisLine","AxisTick","Control","CurrentPointer"],(function(e){this["_render"+e](i,r,a,t)}),this),this._renderAxisLabel(i,o,a,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,e){var n,i,r,o,a=t.get(["label","position"]),s=t.get("orient"),l=function(t,e){return xp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()},t.get("padding"))}(t,e),u={horizontal:"center",vertical:(n=null==a||"auto"===a?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},h={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},c={horizontal:0,vertical:IV/2},p="vertical"===s?l.height:l.width,d=t.getModel("controlStyle"),f=d.get("show",!0),g=f?d.get("itemSize"):0,y=f?d.get("itemGap"):0,v=g+y,m=t.get(["label","rotate"])||0;m=m*IV/180;var x=d.get("position",!0),_=f&&d.get("showPlayBtn",!0),b=f&&d.get("showPrevBtn",!0),w=f&&d.get("showNextBtn",!0),S=0,M=p;"left"===x||"bottom"===x?(_&&(i=[0,0],S+=v),b&&(r=[S,0],S+=v),w&&(o=[M-g,0],M-=v)):(_&&(i=[M-g,0],M-=v),b&&(r=[0,0],S+=v),w&&(o=[M-g,0],M-=v));var I=[S,M];return t.get("inverse")&&I.reverse(),{viewRect:l,mainLength:p,orient:s,rotation:c[s],labelRotation:m,labelPosOpt:n,labelAlign:t.get(["label","align"])||u[s],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||h[s],playPosition:i,prevBtnPosition:r,nextBtnPosition:o,axisExtent:I,controlSize:g,controlGap:y}},e.prototype._position=function(t,e){var n=this._mainGroup,i=this._labelGroup,r=t.viewRect;if("vertical"===t.orient){var o=[1,0,0,1,0,0],a=r.x,s=r.y+r.height;Ei(o,o,[-a,-s]),zi(o,o,-IV/2),Ei(o,o,[a,s]),(r=r.clone()).applyTransform(o)}var l=y(r),u=y(n.getBoundingRect()),h=y(i.getBoundingRect()),c=[n.x,n.y],p=[i.x,i.y];p[0]=c[0]=l[0][0];var d,f=t.labelPosOpt;null==f||X(f)?(v(c,u,l,1,d="+"===f?0:1),v(p,h,l,1,1-d)):(v(c,u,l,1,d=f>=0?0:1),p[1]=c[1]+f);function g(t){t.originX=l[0][0]-t.x,t.originY=l[1][0]-t.y}function y(t){return[[t.x,t.x+t.width],[t.y,t.y+t.height]]}function v(t,e,n,i,r){t[i]+=n[i][r]-e[i][r]}n.setPosition(c),i.setPosition(p),n.rotation=i.rotation=t.rotation,g(n),g(i)},e.prototype._createAxis=function(t,e){var n=e.getData(),i=e.get("axisType"),r=function(t,e){if(e=e||t.get("type"))switch(e){case"category":return new vx({ordinalMeta:t.getCategories(),extent:[1/0,-1/0]});case"time":return new Rx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new xx}}(e,i);r.getTicks=function(){return n.mapArray(["value"],(function(t){return{value:t}}))};var o=n.getDataExtent("value");r.setExtent(o[0],o[1]),r.calcNiceTicks();var a=new MV("value",r,t.axisExtent,i);return a.model=e,a},e.prototype._createGroup=function(t){var e=this[t]=new Cr;return this.group.add(e),e},e.prototype._renderAxisLine=function(t,e,n,i){var r=n.getExtent();if(i.get(["lineStyle","show"])){var o=new zu({shape:{x1:r[0],y1:0,x2:r[1],y2:0},style:A({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});e.add(o);var a=this._progressLine=new zu({shape:{x1:r[0],x2:this._currentPointer?this._currentPointer.x:r[0],y1:0,y2:0},style:k({lineCap:"round",lineWidth:o.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});e.add(a)}},e.prototype._renderAxisTick=function(t,e,n,i){var r=this,o=i.getData(),a=n.scale.getTicks();this._tickSymbols=[],E(a,(function(t){var a=n.dataToCoord(t.value),s=o.getItemModel(t.value),l=s.getModel("itemStyle"),u=s.getModel(["emphasis","itemStyle"]),h=s.getModel(["progress","itemStyle"]),c={x:a,y:0,onclick:W(r._changeTimeline,r,t.value)},p=DV(s,l,e,c);p.ensureState("emphasis").style=u.getItemStyle(),p.ensureState("progress").style=h.getItemStyle(),Ol(p);var d=Hs(p);s.get("tooltip")?(d.dataIndex=t.value,d.dataModel=i):d.dataIndex=d.dataModel=null,r._tickSymbols.push(p)}))},e.prototype._renderAxisLabel=function(t,e,n,i){var r=this;if(n.getLabelModel().get("show")){var o=i.getData(),a=n.getViewLabels();this._tickLabels=[],E(a,(function(i){var a=i.tickValue,s=o.getItemModel(a),l=s.getModel("label"),u=s.getModel(["emphasis","label"]),h=s.getModel(["progress","label"]),c=n.dataToCoord(i.tickValue),p=new ks({x:c,y:0,rotation:t.labelRotation-t.rotation,onclick:W(r._changeTimeline,r,a),silent:!1,style:Uh(l,{text:i.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});p.ensureState("emphasis").style=Uh(u),p.ensureState("progress").style=Uh(h),e.add(p),Ol(p),TV(p).dataIndex=a,r._tickLabels.push(p)}))}},e.prototype._renderControl=function(t,e,n,i){var r=t.controlSize,o=t.rotation,a=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),u=i.get("inverse",!0);function h(t,n,l,u){if(t){var h=gr(rt(i.get(["controlStyle",n+"BtnSize"]),r),r),c=function(t,e,n,i){var r=i.style,o=Ph(t.get(["controlStyle",e]),i||{},new sr(n[0],n[1],n[2],n[3]));r&&o.setStyle(r);return o}(i,n+"Icon",[0,-h/2,h,h],{x:t[0],y:t[1],originX:r/2,originY:0,rotation:u?-o:0,rectHover:!0,style:a,onclick:l});c.ensureState("emphasis").style=s,e.add(c),Ol(c)}}h(t.nextBtnPosition,"next",W(this._changeTimeline,this,u?"-":"+")),h(t.prevBtnPosition,"prev",W(this._changeTimeline,this,u?"+":"-")),h(t.playPosition,l?"stop":"play",W(this._handlePlayClick,this,!l),!0)},e.prototype._renderCurrentPointer=function(t,e,n,i){var r=i.getData(),o=i.getCurrentIndex(),a=r.getItemModel(o).getModel("checkpointStyle"),s=this,l={onCreate:function(t){t.draggable=!0,t.drift=W(s._handlePointerDrag,s),t.ondragend=W(s._handlePointerDragend,s),AV(t,s._progressLine,o,n,i,!0)},onUpdate:function(t){AV(t,s._progressLine,o,n,i)}};this._currentPointer=DV(a,a,this._mainGroup,{},this._currentPointer,l)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,e,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,e){var n=this._toAxisCoord(t)[0],i=Vr(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(a[o]=+a[o].toFixed(c)),[a,h]}var FV={min:H(BV,"min"),max:H(BV,"max"),average:H(BV,"average"),median:H(BV,"median")};function GV(t,e){var n=t.getData(),i=t.coordinateSystem;if(e&&!function(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}(e)&&!Y(e.coord)&&i){var r=i.dimensions,o=WV(e,n,i,t);if((e=T(e)).type&&FV[e.type]&&o.baseAxis&&o.valueAxis){var a=P(r,o.baseAxis.dim),s=P(r,o.valueAxis.dim),l=FV[e.type](n,o.baseDataDim,o.valueDataDim,a,s);e.coord=l[0],e.value=l[1]}else{for(var u=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis],h=0;h<2;h++)FV[u[h]]&&(u[h]=UV(n,n.mapDimension(r[h]),u[h]));e.coord=u}}return e}function WV(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(function(t,e){var n=t.getData().getDimensionInfo(e);return n&&n.coordDim}(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function HV(t,e){return!(t&&t.containData&&e.coord&&!VV(e))||t.containData(e.coord)}function YV(t,e){return t?function(t,n,i,r){return df(r<2?t.coord&&t.coord[r]:t.value,e[r])}:function(t,n,i,r){return df(t.value,e[r])}}function UV(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,(function(t,e){isNaN(t)||(i+=t,r++)})),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e)["max"===n?1:0]}var XV=So(),ZV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){this.markerGroupMap=ft()},e.prototype.render=function(t,e,n){var i=this,r=this.markerGroupMap;r.each((function(t){XV(t).keep=!1})),e.eachSeries((function(t){var r=EV.getMarkerModelFromSeries(t,i.type);r&&i.renderSeries(t,r,e,n)})),r.each((function(t){!XV(t).keep&&i.group.remove(t.group)}))},e.prototype.markKeep=function(t){XV(t).keep=!0},e.prototype.toggleBlurSeries=function(t,e){var n=this;E(t,(function(t){var i=EV.getMarkerModelFromSeries(t,n.type);i&&i.getData().eachItemGraphicEl((function(t){t&&(e?wl(t):Sl(t))}))}))},e.type="marker",e}(gg);function jV(t,e,n){var i=e.coordinateSystem;t.each((function(r){var o,a=t.getItemModel(r),s=Er(a.get("x"),n.getWidth()),l=Er(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(e.getMarkerPosition)o=e.getMarkerPosition(t.getValues(t.dimensions,r));else if(i){var u=t.get(i.dimensions[0],r),h=t.get(i.dimensions[1],r);o=i.dataToPoint([u,h])}}else o=[s,l];isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),t.setItemLayout(r,o)}))}var qV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=EV.getMarkerModelFromSeries(t,"markPoint");e&&(jV(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout())}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new qw),u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new qm(i,n),o=z(n.get("data"),H(GV,e));t&&(o=B(o,H(HV,t)));var a=YV(!!t,i);return r.initData(o,null,a),r}(r,t,e);e.setData(u),jV(e.getData(),t,i),u.each((function(t){var n=u.getItemModel(t),i=n.getShallow("symbol"),r=n.getShallow("symbolSize"),o=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(U(i)||U(r)||U(o)||U(s)){var h=e.getRawValue(t),c=e.getDataParams(t);U(i)&&(i=i(h,c)),U(r)&&(r=r(h,c)),U(o)&&(o=o(h,c)),U(s)&&(s=s(h,c))}var p=n.getModel("itemStyle").getItemStyle(),d=gy(a,"color");p.fill||(p.fill=d),u.setItemVisual(t,{symbol:i,symbolSize:r,symbolRotate:o,symbolOffset:s,symbolKeepAspect:l,style:p})})),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl((function(t){t.traverse((function(t){Hs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markPoint",e}(ZV);var KV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(EV),$V=So(),JV=function(t,e,n,i){var r,o=t.getData();if(Y(i))r=i;else{var a=i.type;if("min"===a||"max"===a||"average"===a||"median"===a||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=e.getAxis(null!=i.yAxis?"y":"x"),l=it(i.yAxis,i.xAxis);else{var u=WV(i,o,e,t);s=u.valueAxis,l=UV(o,ix(o,u.valueDataDim),a)}var h="x"===s.dim?0:1,c=1-h,p=T(i),d={coord:[]};p.type=null,p.coord=[],p.coord[c]=-1/0,d.coord[c]=1/0;var f=n.get("precision");f>=0&&j(l)&&(l=+l.toFixed(Math.min(f,20))),p.coord[h]=d.coord[h]=l,r=[p,d,{type:a,valueIndex:i.valueIndex,value:l}]}else r=[]}var g=[GV(t,r[0]),GV(t,r[1]),A({},r[2])];return g[2].type=g[2].type||null,C(g[2],g[0]),C(g[2],g[1]),g};function QV(t){return!isNaN(t)&&!isFinite(t)}function tB(t,e,n,i){var r=1-t,o=i.dimensions[t];return QV(e[r])&&QV(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function eB(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&(tB(1,n,i,t)||tB(0,n,i,t)))return!0}return HV(t,e[0])&&HV(t,e[1])}function nB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Er(s.get("x"),r.getWidth()),u=Er(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var h=a.dimensions,c=t.get(h[0],e),p=t.get(h[1],e);o=a.dataToPoint([c,p])}if(uS(a,"cartesian2d")){var d=a.getAxis("x"),f=a.getAxis("y");h=a.dimensions;QV(t.get(h[0],e))?o[0]=d.toGlobalCoord(d.getExtent()[n?0:1]):QV(t.get(h[1],e))&&(o[1]=f.toGlobalCoord(f.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}var iB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=EV.getMarkerModelFromSeries(t,"markLine");if(e){var i=e.getData(),r=$V(e).from,o=$V(e).to;r.each((function(e){nB(r,e,!0,t,n),nB(o,e,!1,t,n)})),i.each((function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])})),this.markerGroupMap.get(t.id).updateLayout()}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new gA);this.group.add(l.group);var u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new qm(i,n),o=new qm(i,n),a=new qm([],n),s=z(n.get("data"),H(JV,e,t,n));t&&(s=B(s,H(eB,t)));var l=YV(!!t,i);return r.initData(z(s,(function(t){return t[0]})),null,l),o.initData(z(s,(function(t){return t[1]})),null,l),a.initData(z(s,(function(t){return t[2]}))),a.hasItemOption=!0,{from:r,to:o,line:a}}(r,t,e),h=u.from,c=u.to,p=u.line;$V(e).from=h,$V(e).to=c,e.setData(p);var d=e.get("symbol"),f=e.get("symbolSize"),g=e.get("symbolRotate"),y=e.get("symbolOffset");function v(e,n,r){var o=e.getItemModel(n);nB(e,n,r,t,i);var s=o.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=gy(a,"color")),e.setItemVisual(n,{symbolKeepAspect:o.get("symbolKeepAspect"),symbolOffset:rt(o.get("symbolOffset",!0),y[r?0:1]),symbolRotate:rt(o.get("symbolRotate",!0),g[r?0:1]),symbolSize:rt(o.get("symbolSize"),f[r?0:1]),symbol:rt(o.get("symbol",!0),d[r?0:1]),style:s})}Y(d)||(d=[d,d]),Y(f)||(f=[f,f]),Y(g)||(g=[g,g]),Y(y)||(y=[y,y]),u.from.each((function(t){v(h,t,!0),v(c,t,!1)})),p.each((function(t){var e=p.getItemModel(t).getModel("lineStyle").getLineStyle();p.setItemLayout(t,[h.getItemLayout(t),c.getItemLayout(t)]),null==e.stroke&&(e.stroke=h.getItemVisual(t,"style").fill),p.setItemVisual(t,{fromSymbolKeepAspect:h.getItemVisual(t,"symbolKeepAspect"),fromSymbolOffset:h.getItemVisual(t,"symbolOffset"),fromSymbolRotate:h.getItemVisual(t,"symbolRotate"),fromSymbolSize:h.getItemVisual(t,"symbolSize"),fromSymbol:h.getItemVisual(t,"symbol"),toSymbolKeepAspect:c.getItemVisual(t,"symbolKeepAspect"),toSymbolOffset:c.getItemVisual(t,"symbolOffset"),toSymbolRotate:c.getItemVisual(t,"symbolRotate"),toSymbolSize:c.getItemVisual(t,"symbolSize"),toSymbol:c.getItemVisual(t,"symbol"),style:e})})),l.updateData(p),u.line.eachItemGraphicEl((function(t){Hs(t).dataModel=e,t.traverse((function(t){Hs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markLine",e}(ZV);var rB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(EV),oB=So(),aB=function(t,e,n,i){var r=GV(t,i[0]),o=GV(t,i[1]),a=r.coord,s=o.coord;a[0]=it(a[0],-1/0),a[1]=it(a[1],-1/0),s[0]=it(s[0],1/0),s[1]=it(s[1],1/0);var l=D([{},r,o]);return l.coord=[r.coord,o.coord],l.x0=r.x,l.y0=r.y,l.x1=o.x,l.y1=o.y,l};function sB(t){return!isNaN(t)&&!isFinite(t)}function lB(t,e,n,i){var r=1-t;return sB(e[r])&&sB(n[r])}function uB(t,e){var n=e.coord[0],i=e.coord[1],r={coord:n,x:e.x0,y:e.y0},o={coord:i,x:e.x1,y:e.y1};return uS(t,"cartesian2d")?!(!n||!i||!lB(1,n,i)&&!lB(0,n,i))||function(t,e,n){return!(t&&t.containZone&&e.coord&&n.coord&&!VV(e)&&!VV(n))||t.containZone(e.coord,n.coord)}(t,r,o):HV(t,r)||HV(t,o)}function hB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Er(s.get(n[0]),r.getWidth()),u=Er(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(n,e));else{var h=[d=t.get(n[0],e),f=t.get(n[1],e)];a.clampData&&a.clampData(h,h),o=a.dataToPoint(h,!0)}if(uS(a,"cartesian2d")){var c=a.getAxis("x"),p=a.getAxis("y"),d=t.get(n[0],e),f=t.get(n[1],e);sB(d)?o[0]=c.toGlobalCoord(c.getExtent()["x0"===n[0]?0:1]):sB(f)&&(o[1]=p.toGlobalCoord(p.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}var cB=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],pB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=EV.getMarkerModelFromSeries(t,"markArea");if(e){var i=e.getData();i.each((function(e){var r=z(cB,(function(r){return hB(i,e,r,t,n)}));i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)}))}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,{group:new Cr});this.group.add(l.group),this.markKeep(l);var u=function(t,e,n){var i,r,o=["x0","y0","x1","y1"];if(t){var a=z(t&&t.dimensions,(function(t){var n=e.getData();return A(A({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}));r=z(o,(function(t,e){return{name:t,type:a[e%2].type}})),i=new qm(r,n)}else i=new qm(r=[{name:"value",type:"float"}],n);var s=z(n.get("data"),H(aB,e,t,n));t&&(s=B(s,H(uB,t)));var l=t?function(t,e,n,i){return df(t.coord[Math.floor(i/2)][i%2],r[i])}:function(t,e,n,i){return df(t.value,r[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(r,t,e);e.setData(u),u.each((function(e){var n=z(cB,(function(n){return hB(u,e,n,t,i)})),o=r.getAxis("x").scale,s=r.getAxis("y").scale,l=o.getExtent(),h=s.getExtent(),c=[o.parse(u.get("x0",e)),o.parse(u.get("x1",e))],p=[s.parse(u.get("y0",e)),s.parse(u.get("y1",e))];Vr(c),Vr(p);var d=!!(l[0]>c[1]||l[1]p[1]||h[1]=0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(Tp),fB=H,gB=E,yB=Cr,vB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new yB),this.group.add(this._selectorGroup=new yB),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=t.getBoxLayoutParams(),u={width:n.getWidth(),height:n.getHeight()},h=t.get("padding"),c=xp(l,u,h),p=this.layoutInner(t,r,c,i,a,s),d=xp(k({width:p.width,height:p.height},l),u,h);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=XE(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=ft(),u=e.get("selectedMode"),h=[];n.eachRawSeries((function(t){!t.get("legendHoverLink")&&h.push(t.id)})),gB(e.getData(),(function(r,o){var a=r.get("name");if(!this.newlineDisabled&&(""===a||"\n"===a)){var c=new yB;return c.newline=!0,void s.add(c)}var p=n.getSeriesByName(a)[0];if(!l.get(a)){if(p){var d=p.getData(),f=d.getVisual("legendLineStyle")||{},g=d.getVisual("legendIcon"),y=d.getVisual("style");this._createItem(p,a,o,r,e,t,f,y,g,u,i).on("click",fB(mB,a,null,i,h)).on("mouseover",fB(_B,p.name,null,i,h)).on("mouseout",fB(bB,p.name,null,i,h)),l.set(a,!0)}else n.eachRawSeries((function(n){if(!l.get(a)&&n.legendVisualProvider){var s=n.legendVisualProvider;if(!s.containName(a))return;var c=s.indexOfName(a),p=s.getItemVisual(c,"style"),d=s.getItemVisual(c,"legendIcon"),f=bn(p.fill);f&&0===f[3]&&(f[3]=.2,p=A(A({},p),{fill:kn(f,"rgba")})),this._createItem(n,a,o,r,e,t,{},p,d,u,i).on("click",fB(mB,null,a,i,h)).on("mouseover",fB(_B,null,a,i,h)).on("mouseout",fB(bB,null,a,i,h)),l.set(a,!0)}}),this);0}}),this),r&&this._createSelector(r,e,i,o,a)},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();gB(t,(function(t){var i=t.type,r=new ks({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});o.add(r),Hh(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),Ol(r)}))},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u,h){var c=t.visualDrawType,p=r.get("itemWidth"),d=r.get("itemHeight"),f=r.isSelected(e),g=i.get("symbolRotate"),y=i.get("symbolKeepAspect"),v=i.get("icon"),m=function(t,e,n,i,r,o,a){function s(t,e){"auto"===t.lineWidth&&(t.lineWidth=e.lineWidth>0?2:0),gB(t,(function(n,i){"inherit"===t[i]&&(t[i]=e[i])}))}var l=e.getModel("itemStyle"),u=l.getItemStyle(),h=0===t.lastIndexOf("empty",0)?"fill":"stroke",c=l.getShallow("decal");u.decal=c&&"inherit"!==c?rv(c,a):i.decal,"inherit"===u.fill&&(u.fill=i[r]);"inherit"===u.stroke&&(u.stroke=i[h]);"inherit"===u.opacity&&(u.opacity=("fill"===r?i:n).opacity);s(u,i);var p=e.getModel("lineStyle"),d=p.getLineStyle();if(s(d,n),"auto"===u.fill&&(u.fill=i.fill),"auto"===u.stroke&&(u.stroke=i.fill),"auto"===d.stroke&&(d.stroke=i.fill),!o){var f=e.get("inactiveBorderWidth"),g=u[h];u.lineWidth="auto"===f?i.lineWidth>0&&g?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),d.stroke=p.get("inactiveColor"),d.lineWidth=p.get("inactiveWidth")}return{itemStyle:u,lineStyle:d}}(l=v||l||"roundRect",i,a,s,c,f,h),x=new yB,_=i.getModel("textStyle");if(!U(t.getLegendIcon)||v&&"inherit"!==v){var b="inherit"===v&&t.getData().getVisual("symbol")?"inherit"===g?t.getData().getVisual("symbolRotate"):g:0;x.add(function(t){var e=t.icon||"roundRect",n=Ly(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);n.setStyle(t.itemStyle),n.rotation=(t.iconRotate||0)*Math.PI/180,n.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:p,itemHeight:d,icon:l,iconRotate:b,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}))}else x.add(t.getLegendIcon({itemWidth:p,itemHeight:d,icon:l,iconRotate:g,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}));var w="left"===o?p+5:-5,S=o,M=r.get("formatter"),I=e;X(M)&&M?I=M.replace("{name}",null!=e?e:""):U(M)&&(I=M(e));var T=i.get("inactiveColor");x.add(new ks({style:Uh(_,{text:I,x:w,y:d/2,fill:f?_.getTextColor():T,align:S,verticalAlign:"middle"})}));var C=new Cs({shape:x.getBoundingRect(),invisible:!0}),D=i.getModel("tooltip");return D.get("show")&&Eh({el:C,componentModel:r,itemName:e,itemTooltipOption:D.option}),x.add(C),x.eachChild((function(t){t.silent=!0})),C.silent=!u,this.getContentGroup().add(x),Ol(x),x.__legendDataIndex=n,x},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getContentGroup(),s=this.getSelectorGroup();mp(t.get("orient"),a,t.get("itemGap"),n.width,n.height);var l=a.getBoundingRect(),u=[-l.x,-l.y];if(s.markRedraw(),a.markRedraw(),r){mp("horizontal",s,t.get("selectorItemGap",!0));var h=s.getBoundingRect(),c=[-h.x,-h.y],p=t.get("selectorButtonGap",!0),d=t.getOrient().index,f=0===d?"width":"height",g=0===d?"height":"width",y=0===d?"y":"x";"end"===o?c[d]+=l[f]+p:u[d]+=h[f]+p,c[1-d]+=l[g]/2-h[g]/2,s.x=c[0],s.y=c[1],a.x=u[0],a.y=u[1];var v={x:0,y:0};return v[f]=l[f]+p+h[f],v[g]=Math.max(l[g],h[g]),v[y]=Math.min(0,h[y]+c[1-d]),v}return a.x=u[0],a.y=u[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(gg);function mB(t,e,n,i){bB(t,e,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=t?t:e}),_B(t,e,n,i)}function xB(t){for(var e,n=t.getZr().storage.getDisplayList(),i=0,r=n.length;in[r],f=[-c.x,-c.y];e||(f[i]=l[s]);var g=[0,0],y=[-p.x,-p.y],v=rt(t.get("pageButtonGap",!0),t.get("itemGap",!0));d&&("end"===t.get("pageButtonPosition",!0)?y[i]+=n[r]-p[r]:g[i]+=p[r]+v);y[1-i]+=c[o]/2-p[o]/2,l.setPosition(f),u.setPosition(g),h.setPosition(y);var m={x:0,y:0};if(m[r]=d?n[r]:c[r],m[o]=Math.max(c[o],p[o]),m[a]=Math.min(0,p[a]+y[1-i]),u.__rectSize=n[r],d){var x={x:0,y:0};x[r]=Math.max(n[r]-p[r]-v,0),x[o]=m[o],u.setClipPath(new Cs({shape:x})),u.__rectSize=x[r]}else h.eachChild((function(t){t.attr({invisible:!0,silent:!0})}));var _=this._getPageInfo(t);return null!=_.pageIndex&&rh(l,{x:_.contentPosition[0],y:_.contentPosition[1]},d?t:null),this._updatePageInfoView(t,_),m},e.prototype._pageGo=function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},e.prototype._updatePageInfoView=function(t,e){var n=this._controllerGroup;E(["pagePrev","pageNext"],(function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")}));var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",X(r)?r.replace("{current}",null==a?"":a+"").replace("{total}",null==s?"":s+""):r({current:a,total:s}))},e.prototype._getPageInfo=function(t){var e=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,r=t.getOrient().index,o=DB[r],a=AB[r],s=this._findTargetItemIndex(e),l=n.children(),u=l[s],h=l.length,c=h?1:0,p={contentPosition:[n.x,n.y],pageCount:c,pageIndex:c-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!u)return p;var d=m(u);p.contentPosition[r]=-d.s;for(var f=s+1,g=d,y=d,v=null;f<=h;++f)(!(v=m(l[f]))&&y.e>g.s+i||v&&!x(v,g.s))&&(g=y.i>g.i?y:v)&&(null==p.pageNextDataIndex&&(p.pageNextDataIndex=g.i),++p.pageCount),y=v;for(f=s-1,g=d,y=d,v=null;f>=-1;--f)(v=m(l[f]))&&x(y,v.s)||!(g.i=e&&t.s<=e+i}},e.prototype._findTargetItemIndex=function(t){return this._showController?(this.getContentGroup().eachChild((function(i,r){var o=i.__legendDataIndex;null==n&&null!=o&&(n=r),o===t&&(e=r)})),null!=e?e:n):0;var e,n},e.type="legend.scroll",e}(vB);function LB(t){wm(MB),t.registerComponentModel(IB),t.registerComponentView(kB),function(t){t.registerAction("legendScroll","legendscroll",(function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},(function(t){t.setScrollDataIndex(n)}))}))}(t)}var PB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.inside",e.defaultOption=yc(AE.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(AE),OB=So();function RB(t,e,n){OB(t).coordSysRecordMap.each((function(t){var i=t.dataZoomInfoMap.get(e.uid);i&&(i.getRange=n)}))}function NB(t,e){if(e){t.removeKey(e.model.uid);var n=e.controller;n&&n.dispose()}}function EB(t,e){t.isDisposed()||t.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:e})}function zB(t,e,n,i){return t.coordinateSystem.containPoint([n,i])}function VB(t){t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,(function(t,e){var n=OB(e),i=n.coordSysRecordMap||(n.coordSysRecordMap=ft());i.each((function(t){t.dataZoomInfoMap=null})),t.eachComponent({mainType:"dataZoom",subType:"inside"},(function(t){E(CE(t).infoList,(function(n){var r=n.model.uid,o=i.get(r)||i.set(r,function(t,e){var n={model:e,containsPoint:H(zB,e),dispatchAction:H(EB,t),dataZoomInfoMap:null,controller:null},i=n.controller=new kI(t.getZr());return E(["pan","zoom","scrollMove"],(function(t){i.on(t,(function(e){var i=[];n.dataZoomInfoMap.each((function(r){if(e.isAvailableBehavior(r.model.option)){var o=(r.getRange||{})[t],a=o&&o(r.dzReferCoordSysInfo,n.model.mainType,n.controller,e);!r.model.get("disabled",!0)&&a&&i.push({dataZoomId:r.model.id,start:a[0],end:a[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(e,n.model));(o.dataZoomInfoMap||(o.dataZoomInfoMap=ft())).set(t.uid,{dzReferCoordSysInfo:n,model:t,getRange:null})}))})),i.each((function(t){var e,n=t.controller,r=t.dataZoomInfoMap;if(r){var o=r.keys()[0];null!=o&&(e=r.get(o))}if(e){var a=function(t){var e,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},r=!0;return t.each((function(t){var o=t.model,a=!o.get("disabled",!0)&&(!o.get("zoomLock",!0)||"move");i[n+a]>i[n+e]&&(e=a),r=r&&o.get("preventDefaultMouseMove",!0)})),{controlType:e,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!r}}}(r);n.enable(a.controlType,a.opt),n.setPointerChecker(t.containsPoint),Ag(t,"dispatchAction",e.model.get("throttle",!0),"fixRate")}else NB(i,t)}))}))}var BB=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="dataZoom.inside",e}return n(e,t),e.prototype.render=function(e,n,i){t.prototype.render.apply(this,arguments),e.noTarget()?this._clear():(this.range=e.getPercentRange(),RB(i,e,{pan:W(FB.pan,this),zoom:W(FB.zoom,this),scrollMove:W(FB.scrollMove,this)}))},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){!function(t,e){for(var n=OB(t).coordSysRecordMap,i=n.keys(),r=0;r0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(o[1]-o[0])+o[0],u=Math.max(1/i.scale,0);o[0]=(o[0]-l)*u+l,o[1]=(o[1]-l)*u+l;var h=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return lk(0,o,[0,100],0,h.minSpan,h.maxSpan),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}},pan:GB((function(t,e,n,i,r,o){var a=WB[i]([o.oldX,o.oldY],[o.newX,o.newY],e,r,n);return a.signal*(t[1]-t[0])*a.pixel/a.pixelLength})),scrollMove:GB((function(t,e,n,i,r,o){return WB[i]([0,0],[o.scrollDelta,o.scrollDelta],e,r,n).signal*(t[1]-t[0])*o.scrollDelta}))};function GB(t){return function(e,n,i,r){var o=this.range,a=o.slice(),s=e.axisModels[0];if(s)return lk(t(a,s,e,n,i,r),a,[0,100],"all"),this.range=a,o[0]!==a[0]||o[1]!==a[1]?a:void 0}}var WB={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};function HB(t){BE(t),t.registerComponentModel(PB),t.registerComponentView(BB),VB(t)}var YB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=yc(AE.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),e}(AE),UB=Cs,XB="horizontal",ZB="vertical",jB=["line","bar","candlestick","scatter"],qB={easing:"cubicOut",duration:100,delay:0},KB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._displayables={},n}return n(e,t),e.prototype.init=function(t,e){this.api=e,this._onBrush=W(this._onBrush,this),this._onBrushEnd=W(this._onBrushEnd,this)},e.prototype.render=function(e,n,i,r){if(t.prototype.render.apply(this,arguments),Ag(this,"_dispatchZoomAction",e.get("throttle"),"fixRate"),this._orient=e.getOrient(),!1!==e.get("show")){if(e.noTarget())return this._clear(),void this.group.removeAll();r&&"dataZoom"===r.type&&r.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){kg(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var e=this._displayables.sliderGroup=new Cr;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,e=this.api,n=t.get("brushSelect")?7:0,i=this._findCoordRect(),r={width:e.getWidth(),height:e.getHeight()},o=this._orient===XB?{right:r.width-i.x-i.width,top:r.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},a=Sp(t.option);E(["right","top","width","height"],(function(t){"ph"===a[t]&&(a[t]=o[t])}));var s=xp(a,r);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===ZB&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.sliderGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==XB||r?n===XB&&r?{scaleY:a?1:-1,scaleX:-1}:n!==ZB||r?{scaleY:a?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:a?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:a?1:-1,scaleX:1});var s=t.getBoundingRect([o]);t.x=e.x-s.x,t.y=e.y-s.y,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new UB({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var r=new UB({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:W(this._onClickPanel,this)}),o=this.api.getZr();i?(r.on("mousedown",this._onBrushStart,this),r.cursor="crosshair",o.on("mousemove",this._onBrush),o.on("mouseup",this._onBrushEnd)):(o.off("mousemove",this._onBrush),o.off("mouseup",this._onBrushEnd)),n.add(r)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],t){var e=this._size,n=this._shadowSize||[],i=t.series,r=i.getRawData(),o=i.getShadowDim?i.getShadowDim():t.otherDim;if(null!=o){var a=this._shadowPolygonPts,s=this._shadowPolylinePts;if(r!==this._shadowData||o!==this._shadowDim||e[0]!==n[0]||e[1]!==n[1]){var l=r.getDataExtent(o),u=.3*(l[1]-l[0]);l=[l[0]-u,l[1]+u];var h,c=[0,e[1]],p=[0,e[0]],d=[[e[0],0],[0,0]],f=[],g=p[1]/(r.count()-1),y=0,v=Math.round(r.count()/e[0]);r.each([o],(function(t,e){if(v>0&&e%v)y+=g;else{var n=null==t||isNaN(t)||""===t,i=n?0:Nr(t,l,c,!0);n&&!h&&e?(d.push([d[d.length-1][0],0]),f.push([f[f.length-1][0],0])):!n&&h&&(d.push([y,0]),f.push([y,0])),d.push([y,i]),f.push([y,i]),y+=g,h=n}})),a=this._shadowPolygonPts=d,s=this._shadowPolylinePts=f}this._shadowData=r,this._shadowDim=o,this._shadowSize=[e[0],e[1]];for(var m=this.dataZoomModel,x=0;x<3;x++){var _=b(1===x);this._displayables.sliderGroup.add(_),this._displayables.dataShadowSegs.push(_)}}}function b(t){var e=m.getModel(t?"selectedDataBackground":"dataBackground"),n=new Cr,i=new Pu({shape:{points:a},segmentIgnoreThreshold:1,style:e.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),r=new Ru({shape:{points:s},segmentIgnoreThreshold:1,style:e.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(r),n}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis((function(r,o){E(t.getAxisProxy(r,o).getTargetSeriesModels(),(function(t){if(!(n||!0!==e&&P(jB,t.get("type"))<0)){var a,s=i.getComponent(IE(r),o).axis,l={x:"y",y:"x",radius:"angle",angle:"radius"}[r],u=t.coordinateSystem;null!=l&&u.getOtherAxis&&(a=u.getOtherAxis(s).inverse),l=t.getData().mapDimension(l),n={thisAxis:s,series:t,thisDim:r,otherDim:l,otherAxisInverse:a}}}),this)}),this),n}},e.prototype._renderHandle=function(){var t=this.group,e=this._displayables,n=e.handles=[null,null],i=e.handleLabels=[null,null],r=this._displayables.sliderGroup,o=this._size,a=this.dataZoomModel,s=this.api,l=a.get("borderRadius")||0,u=a.get("brushSelect"),h=e.filler=new UB({silent:u,style:{fill:a.get("fillerColor")},textConfig:{position:"inside"}});r.add(h),r.add(new UB({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:o[0],height:o[1],r:l},style:{stroke:a.get("dataBackgroundColor")||a.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),E([0,1],(function(e){var o=a.get("handleIcon");!Dy[o]&&o.indexOf("path://")<0&&o.indexOf("image://")<0&&(o="path://"+o);var s=Ly(o,-1,0,2,2,null,!0);s.attr({cursor:$B(this._orient),draggable:!0,drift:W(this._onDragMove,this,e),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),u=a.get("handleSize");this._handleHeight=Er(u,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(a.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=a.getModel(["emphasis","handleStyle"]).getItemStyle(),Ol(s);var h=a.get("handleColor");null!=h&&(s.style.fill=h),r.add(n[e]=s);var c=a.getModel("textStyle");t.add(i[e]=new ks({silent:!0,invisible:!0,style:Uh(c,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:c.getTextColor(),font:c.getFont()}),z2:10}))}),this);var c=h;if(u){var p=Er(a.get("moveHandleSize"),o[1]),d=e.moveHandle=new Cs({style:a.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:o[1]-.5,height:p}}),f=.8*p,g=e.moveHandleIcon=Ly(a.get("moveHandleIcon"),-f/2,-f/2,f,f,"#fff",!0);g.silent=!0,g.y=o[1]+p/2-.5,d.ensureState("emphasis").style=a.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var y=Math.min(o[1]/2,Math.max(p,10));(c=e.moveZone=new Cs({invisible:!0,shape:{y:o[1]-y,height:p+y}})).on("mouseover",(function(){s.enterEmphasis(d)})).on("mouseout",(function(){s.leaveEmphasis(d)})),r.add(d),r.add(g),r.add(c)}c.attr({draggable:!0,cursor:$B(this._orient),drift:W(this._onDragMove,this,"all"),ondragstart:W(this._showDataInfo,this,!0),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[Nr(t[0],[0,100],e,!0),Nr(t[1],[0,100],e,!0)]},e.prototype._updateInterval=function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];lk(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?Nr(o.minSpan,a,r,!0):null,null!=o.maxSpan?Nr(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=Vr([Nr(i[0],r,a,!0),Nr(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},e.prototype._updateView=function(t){var e=this._displayables,n=this._handleEnds,i=Vr(n.slice()),r=this._size;E([0,1],(function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scaleX:o/2,scaleY:o/2,x:n[t]+(t?-1:1),y:r[1]/2-o/2})}),this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]});var o={x:i[0],width:i[1]-i[0]};e.moveHandle&&(e.moveHandle.setShape(o),e.moveZone.setShape(o),e.moveZone.getBoundingRect(),e.moveHandleIcon&&e.moveHandleIcon.attr("x",o.x+o.width/2));for(var a=e.dataShadowSegs,s=[0,i[0],i[1],r[0]],l=0;le[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var e=t.offsetX,n=t.offsetY;this._brushStart=new Ji(e,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var e=this._displayables.brushRect;if(this._brushing=!1,e){e.attr("ignore",!0);var n=e.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),r=[0,100];this._range=Vr([Nr(n.x,i,r,!0),Nr(n.x+n.width,i,r,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(se(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,e){var n=this._displayables,i=this.dataZoomModel,r=n.brushRect;r||(r=n.brushRect=new UB({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(r)),r.attr("ignore",!1);var o=this._brushStart,a=this._displayables.sliderGroup,s=a.transformCoordToLocal(t,e),l=a.transformCoordToLocal(o.x,o.y),u=this._size;s[0]=Math.max(Math.min(u[0],s[0]),0),r.setShape({x:l[0],y:0,width:s[0]-l[0],height:u[1]})},e.prototype._dispatchZoomAction=function(t){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?qB:null,start:e[0],end:e[1]})},e.prototype._findCoordRect=function(){var t,e=CE(this.dataZoomModel).infoList;if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),r=this.api.getHeight();t={x:.2*i,y:.2*r,width:.6*i,height:.6*r}}return t},e.type="dataZoom.slider",e}(PE);function $B(t){return"vertical"===t?"ns-resize":"ew-resize"}function JB(t){t.registerComponentModel(YB),t.registerComponentView(KB),BE(t)}var QB=function(t,e,n){var i=T((tF[t]||{})[e]);return n&&Y(i)?i[i.length-1]:i},tF={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},eF=iD.mapVisual,nF=iD.eachVisual,iF=Y,rF=E,oF=Vr,aF=Nr,sF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&eV(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var e=this.stateList;t=W(t,this),this.controllerVisuals=tV(this.option.controller,e,t),this.targetVisuals=tV(this.option.target,e,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this.option.seriesIndex,e=[];return null==t||"all"===t?this.ecModel.eachSeries((function(t,n){e.push(n)})):e=ho(t),e},e.prototype.eachTargetSeries=function(t,e){E(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(e,i)}),this)},e.prototype.isTargetSeries=function(t){var e=!1;return this.eachTargetSeries((function(n){n===t&&(e=!0)})),e},e.prototype.formatValueText=function(t,e,n){var i,r=this.option,o=r.precision,a=this.dataBound,s=r.formatter;n=n||["<",">"],Y(t)&&(t=t.slice(),i=!0);var l=e?t:i?[u(t[0]),u(t[1])]:u(t);return X(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):U(s)?i?s(t[0],t[1]):s(t):i?t[0]===a[0]?n[0]+" "+l[1]:t[1]===a[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function u(t){return t===a[0]?"min":t===a[1]?"max":(+t).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,e=oF([t.min,t.max]);this._dataExtent=e},e.prototype.getDataDimensionIndex=function(t){var e=this.option.dimension;if(null!=e)return t.getDimensionIndex(e);for(var n=t.dimensions,i=n.length-1;i>=0;i--){var r=n[i],o=t.getDimensionInfo(r);if(!o.isCalculationCoord)return o.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,e=this.option,n={inRange:e.inRange,outOfRange:e.outOfRange},i=e.target||(e.target={}),r=e.controller||(e.controller={});C(i,n),C(r,n);var o=this.isCategory();function a(n){iF(e.color)&&!n.inRange&&(n.inRange={color:e.color.slice().reverse()}),n.inRange=n.inRange||{color:t.get("gradientColor")}}a.call(this,i),a.call(this,r),function(t,e,n){var i=t[e],r=t[n];i&&!r&&(r=t[n]={},rF(i,(function(t,e){if(iD.isValidType(e)){var n=QB(e,"inactive",o);null!=n&&(r[e]=n,"color"!==e||r.hasOwnProperty("opacity")||r.hasOwnProperty("colorAlpha")||(r.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(t){var e=(t.inRange||{}).symbol||(t.outOfRange||{}).symbol,n=(t.inRange||{}).symbolSize||(t.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),r=this.getItemSymbol()||"roundRect";rF(this.stateList,(function(a){var s=this.itemSize,l=t[a];l||(l=t[a]={color:o?i:[i]}),null==l.symbol&&(l.symbol=e&&T(e)||(o?r:[r])),null==l.symbolSize&&(l.symbolSize=n&&T(n)||(o?s[0]:[s[0],s[0]])),l.symbol=eF(l.symbol,(function(t){return"none"===t?r:t}));var u=l.symbolSize;if(null!=u){var h=-1/0;nF(u,(function(t){t>h&&(h=t)})),l.symbolSize=eF(u,(function(t){return aF(t,[0,h],[0,s[0]],!0)}))}}),this)}.call(this,r)},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},e}(Tp),lF=[20,140],uF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(t){t.mappingMethod="linear",t.dataExtent=this.getExtent()})),this._resetRange()},e.prototype.resetItemSize=function(){t.prototype.resetItemSize.apply(this,arguments);var e=this.itemSize;(null==e[0]||isNaN(e[0]))&&(e[0]=lF[0]),(null==e[1]||isNaN(e[1]))&&(e[1]=lF[1])},e.prototype._resetRange=function(){var t=this.getExtent(),e=this.option.range;!e||e.auto?(t.auto=1,this.option.range=t):Y(e)&&(e[0]>e[1]&&e.reverse(),e[0]=Math.max(e[0],t[0]),e[1]=Math.min(e[1],t[1]))},e.prototype.completeVisualOption=function(){t.prototype.completeVisualOption.apply(this,arguments),E(this.stateList,(function(t){var e=this.option.controller[t].symbolSize;e&&e[0]!==e[1]&&(e[0]=e[1]/3)}),this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),e=Vr((this.get("range")||[]).slice());return e[0]>t[1]&&(e[0]=t[1]),e[1]>t[1]&&(e[1]=t[1]),e[0]=n[1]||t<=e[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[];return this.eachTargetSeries((function(n){var i=[],r=n.getData();r.each(this.getDataDimensionIndex(r),(function(e,n){t[0]<=e&&e<=t[1]&&i.push(n)}),this),e.push({seriesId:n.id,dataIndex:i})}),this),e},e.prototype.getVisualMeta=function(t){var e=hF(this,"outOfRange",this.getExtent()),n=hF(this,"inRange",this.option.range.slice()),i=[];function r(e,n){i.push({value:e,color:t(e,n)})}for(var o=0,a=0,s=n.length,l=e.length;at[1])break;n.push({color:this.getControllerVisual(o,"color",e),offset:r/100})}return n.push({color:this.getControllerVisual(t[1],"color",e),offset:1}),n},e.prototype._createBarPoints=function(t,e){var n=this.visualMapModel.itemSize;return[[n[0]-e[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-e[1],t[1]]]},e.prototype._createBarGroup=function(t){var e=this._orient,n=this.visualMapModel.get("inverse");return new Cr("horizontal"!==e||n?"horizontal"===e&&n?{scaleX:"bottom"===t?-1:1,rotation:-Math.PI/2}:"vertical"!==e||n?{scaleX:"left"===t?1:-1}:{scaleX:"left"===t?1:-1,scaleY:-1}:{scaleX:"bottom"===t?1:-1,rotation:Math.PI/2})},e.prototype._updateHandle=function(t,e){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,r=n.handleThumbs,o=n.handleLabels,a=i.itemSize,s=i.getExtent();yF([0,1],(function(l){var u=r[l];u.setStyle("fill",e.handlesColor[l]),u.y=t[l];var h=gF(t[l],[0,a[1]],s,!0),c=this.getControllerVisual(h,"symbolSize");u.scaleX=u.scaleY=c/a[0],u.x=a[0]-c/2;var p=Th(n.handleLabelPoints[l],Ih(u,this.group));o[l].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},e.prototype._showIndicator=function(t,e,n,i){var r=this.visualMapModel,o=r.getExtent(),a=r.itemSize,s=[0,a[1]],l=this._shapes,u=l.indicator;if(u){u.attr("invisible",!1);var h=this.getControllerVisual(t,"color",{convertOpacityToAlpha:!0}),c=this.getControllerVisual(t,"symbolSize"),p=gF(t,o,s,!0),d=a[0]-c/2,f={x:u.x,y:u.y};u.y=p,u.x=d;var g=Th(l.indicatorLabelPoint,Ih(u,this.group)),y=l.indicatorLabel;y.attr("invisible",!1);var v=this._applyTransform("left",l.mainGroup),m="horizontal"===this._orient;y.setStyle({text:(n||"")+r.formatValueText(e),verticalAlign:m?v:"middle",align:m?"center":v});var x={x:d,y:p,style:{fill:h}},_={style:{x:g[0],y:g[1]}};if(r.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var b={duration:100,easing:"cubicInOut",additive:!0};u.x=f.x,u.y=f.y,u.animateTo(x,b),y.animateTo(_,b)}else u.attr(x),y.attr(_);this._firstShowIndicator=!1;var w=this._shapes.handleLabels;if(w)for(var S=0;Sr[1]&&(u[1]=1/0),e&&(u[0]===-1/0?this._showIndicator(l,u[1],"< ",a):u[1]===1/0?this._showIndicator(l,u[0],"> ",a):this._showIndicator(l,l,"≈ ",a));var h=this._hoverLinkDataIndices,c=[];(e||bF(n))&&(c=this._hoverLinkDataIndices=n.findTargetDataIndices(u));var p=function(t,e){var n={},i={};return r(t||[],n),r(e||[],i,n),[o(n),o(i)];function r(t,e,n){for(var i=0,r=t.length;i=0&&(r.dimension=o,i.push(r))}})),t.getData().setVisual("visualMeta",i)}}];function TF(t,e,n,i){for(var r=e.targetVisuals[i],o=iD.prepareVisualTypes(r),a={color:gy(t.getData(),"color")},s=0,l=o.length;s0:t.splitNumber>0)&&!t.calculable?"piecewise":"continuous"})),t.registerAction(SF,MF),E(IF,(function(e){t.registerVisual(t.PRIORITY.VISUAL.COMPONENT,e)})),t.registerPreprocessor(DF))}function PF(t){t.registerComponentModel(uF),t.registerComponentView(xF),LF(t)}var OF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._pieceList=[],n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],RF[this._mode].call(this,this._pieceList),this._resetSelected(e,n);var r=this.option.categories;this.resetVisual((function(t,e){"categories"===i?(t.mappingMethod="category",t.categories=T(r)):(t.dataExtent=this.getExtent(),t.mappingMethod="piecewise",t.pieceList=z(this._pieceList,(function(t){return t=T(t),"inRange"!==e&&(t.visual=null),t})))}))},e.prototype.completeVisualOption=function(){var e=this.option,n={},i=iD.listVisualTypes(),r=this.isCategory();function o(t,e,n){return t&&t[e]&&t[e].hasOwnProperty(n)}E(e.pieces,(function(t){E(i,(function(e){t.hasOwnProperty(e)&&(n[e]=1)}))})),E(n,(function(t,n){var i=!1;E(this.stateList,(function(t){i=i||o(e,t,n)||o(e.target,t,n)}),this),!i&&E(this.stateList,(function(t){(e[t]||(e[t]={}))[n]=QB(n,"inRange"===t?"active":"inactive",r)}))}),this),t.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,e){var n=this.option,i=this._pieceList,r=(e?n:t).selected||{};if(n.selected=r,E(i,(function(t,e){var n=this.getSelectedMapKey(t);r.hasOwnProperty(n)||(r[n]=!0)}),this),"single"===n.selectedMode){var o=!1;E(i,(function(t,e){var n=this.getSelectedMapKey(t);r[n]&&(o?r[n]=!1:o=!0)}),this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return"categories"===this._mode?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=T(t)},e.prototype.getValueState=function(t){var e=iD.findPieceIndex(t,this._pieceList);return null!=e&&this.option.selected[this.getSelectedMapKey(this._pieceList[e])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[],n=this._pieceList;return this.eachTargetSeries((function(i){var r=[],o=i.getData();o.each(this.getDataDimensionIndex(o),(function(e,i){iD.findPieceIndex(e,n)===t&&r.push(i)}),this),e.push({seriesId:i.id,dataIndex:r})}),this),e},e.prototype.getRepresentValue=function(t){var e;if(this.isCategory())e=t.value;else if(null!=t.value)e=t.value;else{var n=t.interval||[];e=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return e},e.prototype.getVisualMeta=function(t){if(!this.isCategory()){var e=[],n=["",""],i=this,r=this._pieceList.slice();if(r.length){var o=r[0].interval[0];o!==-1/0&&r.unshift({interval:[-1/0,o]}),(o=r[r.length-1].interval[1])!==1/0&&r.push({interval:[o,1/0]})}else r.push({interval:[-1/0,1/0]});var a=-1/0;return E(r,(function(t){var e=t.interval;e&&(e[0]>a&&s([a,e[0]],"outOfRange"),s(e.slice()),a=e[1])}),this),{stops:e,outerColors:n}}function s(r,o){var a=i.getRepresentValue({interval:r});o||(o=i.getValueState(a));var s=t(a,o);r[0]===-1/0?n[0]=s:r[1]===1/0?n[1]=s:e.push({value:r[0],color:s},{value:r[1],color:s})}},e.type="visualMap.piecewise",e.defaultOption=yc(sF.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(sF),RF={splitNumber:function(t){var e=this.option,n=Math.min(e.precision,20),i=this.getExtent(),r=e.splitNumber;r=Math.max(parseInt(r,10),1),e.splitNumber=r;for(var o=(i[1]-i[0])/r;+o.toFixed(n)!==o&&n<5;)n++;e.precision=n,o=+o.toFixed(n),e.minOpen&&t.push({interval:[-1/0,i[0]],close:[0,0]});for(var a=0,s=i[0];a","≥"][e[0]]];t.text=t.text||this.formatValueText(null!=t.value?t.value:t.interval,!1,n)}),this)}};function NF(t,e){var n=t.inverse;("vertical"===t.orient?!n:n)&&e.reverse()}var EF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.doRender=function(){var t=this.group;t.removeAll();var e=this.visualMapModel,n=e.get("textGap"),i=e.textStyleModel,r=i.getFont(),o=i.getTextColor(),a=this._getItemAlign(),s=e.itemSize,l=this._getViewData(),u=l.endsText,h=it(e.get("showLabel",!0),!u);u&&this._renderEndsText(t,u[0],s,h,a),E(l.viewPieceList,(function(i){var l=i.piece,u=new Cr;u.onclick=W(this._onItemClick,this,l),this._enableHoverLink(u,i.indexInModelPieceList);var c=e.getRepresentValue(l);if(this._createItemSymbol(u,c,[0,0,s[0],s[1]]),h){var p=this.visualMapModel.getValueState(c);u.add(new ks({style:{x:"right"===a?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:a,font:r,fill:o,opacity:"outOfRange"===p?.5:1}}))}t.add(u)}),this),u&&this._renderEndsText(t,u[1],s,h,a),mp(e.get("orient"),t,e.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,e){var n=this;t.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(t){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:t,batch:fF(i.findTargetDataIndices(e),i)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,e=t.option;if("vertical"===e.orient)return dF(t,this.api,t.itemSize);var n=e.align;return n&&"auto"!==n||(n="left"),n},e.prototype._renderEndsText=function(t,e,n,i,r){if(e){var o=new Cr,a=this.visualMapModel.textStyleModel;o.add(new ks({style:Uh(a,{x:i?"right"===r?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?r:"center",text:e})})),t.add(o)}},e.prototype._getViewData=function(){var t=this.visualMapModel,e=z(t.getPieceList(),(function(t,e){return{piece:t,indexInModelPieceList:e}})),n=t.get("text"),i=t.get("orient"),r=t.get("inverse");return("horizontal"===i?r:!r)?e.reverse():n&&(n=n.slice().reverse()),{viewPieceList:e,endsText:n}},e.prototype._createItemSymbol=function(t,e,n){t.add(Ly(this.getControllerVisual(e,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(e,"color")))},e.prototype._onItemClick=function(t){var e=this.visualMapModel,n=e.option,i=n.selectedMode;if(i){var r=T(n.selected),o=e.getSelectedMapKey(t);"single"===i||!0===i?(r[o]=!0,E(r,(function(t,e){r[e]=e===o}))):r[o]=!r[o],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:r})}},e.type="visualMap.piecewise",e}(cF);function zF(t){t.registerComponentModel(OF),t.registerComponentView(EF),LF(t)}var VF={label:{enabled:!0},decal:{show:!1}},BF=So(),FF={};function GF(t,e){var n=t.getModel("aria");if(n.get("enabled")){var i=T(VF);C(i.label,t.getLocaleModel().get("aria"),!1),C(n.option,i,!1),function(){if(n.getModel("decal").get("show")){var e=ft();t.eachSeries((function(t){if(!t.isColorBySeries()){var n=e.get(t.type);n||(n={},e.set(t.type,n)),BF(t).scope=n}})),t.eachRawSeries((function(e){if(!t.isSeriesFiltered(e))if(U(e.enableAriaDecal))e.enableAriaDecal();else{var n=e.getData();if(e.isColorBySeries()){var i=ed(e.ecModel,e.name,FF,t.getSeriesCount()),r=n.getVisual("decal");n.setVisual("decal",u(r,i))}else{var o=e.getRawData(),a={},s=BF(e).scope;n.each((function(t){var e=n.getRawIndex(t);a[e]=t}));var l=o.count();o.each((function(t){var i=a[t],r=o.getName(t)||t+"",h=ed(e.ecModel,r,s,l),c=n.getItemVisual(i,"decal");n.setItemVisual(i,"decal",u(c,h))}))}}function u(t,e){var n=t?A(A({},e),t):e;return n.dirty=!0,n}}))}}(),function(){var i=t.getLocaleModel().get("aria"),o=n.getModel("label");if(o.option=k(o.option,i),!o.get("enabled"))return;var a=e.getZr().dom;if(o.get("description"))return void a.setAttribute("aria-label",o.get("description"));var s,l=t.getSeriesCount(),u=o.get(["data","maxCount"])||10,h=o.get(["series","maxCount"])||10,c=Math.min(l,h);if(l<1)return;var p=function(){var e=t.get("title");e&&e.length&&(e=e[0]);return e&&e.text}();if(p){var d=o.get(["general","withTitle"]);s=r(d,{title:p})}else s=o.get(["general","withoutTitle"]);var f=[],g=l>1?o.get(["series","multiple","prefix"]):o.get(["series","single","prefix"]);s+=r(g,{seriesCount:l}),t.eachSeries((function(e,n){if(n1?o.get(["series","multiple",a]):o.get(["series","single",a]),{seriesId:e.seriesIndex,seriesName:e.get("name"),seriesType:(x=e.subType,t.getLocaleModel().get(["series","typeNames"])[x]||"自定义图")});var s=e.getData();if(s.count()>u)i+=r(o.get(["data","partialData"]),{displayCnt:u});else i+=o.get(["data","allData"]);for(var h=o.get(["data","separator","middle"]),p=o.get(["data","separator","end"]),d=[],g=0;g":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},YF=function(){function t(t){if(null==(this._condVal=X(t)?new RegExp(t):et(t)?t:null)){var e="";0,ao(e)}}return t.prototype.evaluate=function(t){var e=typeof t;return X(e)?this._condVal.test(t):!!j(e)&&this._condVal.test(t+"")},t}(),UF=function(){function t(){}return t.prototype.evaluate=function(){return this.value},t}(),XF=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e2&&l.push(e),e=[t,n]}function f(t,n,i,r){oG(t,i)&&oG(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=nM:C2&&l.push(e),l}function sG(t,e,n,i,r,o,a,s,l,u){if(oG(t,n)&&oG(e,i)&&oG(r,a)&&oG(o,s))l.push(a,s);else{var h=2/u,c=h*h,p=a-t,d=s-e,f=Math.sqrt(p*p+d*d);p/=f,d/=f;var g=n-t,y=i-e,v=r-a,m=o-s,x=g*g+y*y,_=v*v+m*m;if(x=0&&_-w*w=0)l.push(a,s);else{var S=[],M=[];Ze(t,n,r,a,.5,S),Ze(e,i,o,s,.5,M),sG(S[0],M[0],S[1],M[1],S[2],M[2],S[3],M[3],l,u),sG(S[4],M[4],S[5],M[5],S[6],M[6],S[7],M[7],l,u)}}}}function lG(t,e,n){var i=t[e],r=t[1-e],o=Math.abs(i/r),a=Math.ceil(Math.sqrt(o*n)),s=Math.floor(n/a);0===s&&(s=1,a=n);for(var l=[],u=0;u0)for(u=0;uMath.abs(u),c=lG([l,u],h?0:1,e),p=(h?s:u)/c.length,d=0;d1?null:new Ji(d*l+t,d*u+e)}function pG(t,e,n){var i=new Ji;Ji.sub(i,n,e),i.normalize();var r=new Ji;return Ji.sub(r,t,e),r.dot(i)}function dG(t,e){var n=t[t.length-1];n&&n[0]===e[0]&&n[1]===e[1]||t.push(e)}function fG(t){var e=t.points,n=[],i=[];Ma(e,n,i);var r=new sr(n[0],n[1],i[0]-n[0],i[1]-n[1]),o=r.width,a=r.height,s=r.x,l=r.y,u=new Ji,h=new Ji;return o>a?(u.x=h.x=s+o/2,u.y=l,h.y=l+a):(u.y=h.y=l+a/2,u.x=s,h.x=s+o),function(t,e,n){for(var i=t.length,r=[],o=0;or,a=lG([i,r],o?0:1,e),s=o?"width":"height",l=o?"height":"width",u=o?"x":"y",h=o?"y":"x",c=t[s]/a.length,p=0;p0)for(var b=i/n,w=-i/2;w<=i/2;w+=b){var S=Math.sin(w),M=Math.cos(w),I=0;for(x=0;x0;l/=2){var u=0,h=0;(t&l)>0&&(u=1),(e&l)>0&&(h=1),s+=l*l*(3*u^h),0===h&&(1===u&&(t=l-1-t,e=l-1-e),a=t,t=e,e=a)}return s}function LG(t){var e=1/0,n=1/0,i=-1/0,r=-1/0,o=z(t,(function(t){var o=t.getBoundingRect(),a=t.getComputedTransform(),s=o.x+o.width/2+(a?a[4]:0),l=o.y+o.height/2+(a?a[5]:0);return e=Math.min(s,e),n=Math.min(l,n),i=Math.max(s,i),r=Math.max(l,r),[s,l]}));return z(o,(function(o,a){return{cp:o,z:kG(o[0],o[1],e,n,i,r),path:t[a]}})).sort((function(t,e){return t.z-e.z})).map((function(t){return t.path}))}function PG(t){return vG(t.path,t.count)}function OG(t){return Y(t[0])}function RG(t,e){for(var n=[],i=t.length,r=0;r=0;r--)if(!n[r].many.length){var l=n[s].many;if(l.length<=1){if(!s)return n;s=0}o=l.length;var u=Math.ceil(o/2);n[r].many=l.slice(u,o),n[s].many=l.slice(0,u),s++}return n}var NG={clone:function(t){for(var e=[],n=1-Math.pow(1-t.path.style.opacity,1/t.count),i=0;i0){var s,l,u=i.getModel("universalTransition").get("delay"),h=Object.assign({setToFinal:!0},a);OG(t)&&(s=t,l=e),OG(e)&&(s=e,l=t);for(var c=s?s===t:t.length>e.length,p=s?RG(l,s):RG(c?e:t,[c?t:e]),d=0,f=0;f1e4))for(var i=n.getIndices(),r=function(t){for(var e=t.dimensions,n=0;n0&&i.group.traverse((function(t){t instanceof gs&&!t.animators.length&&t.animateFrom({style:{opacity:0}},r)}))}))}function UG(t){var e=t.getModel("universalTransition").get("seriesKey");return e||t.id}function XG(t){return Y(t)?t.sort().join(","):t}function ZG(t){if(t.hostModel)return t.hostModel.getModel("universalTransition").get("divideShape")}function jG(t,e){for(var n=0;n=0&&r.push({data:e.oldData[n],divide:ZG(e.oldData[n]),dim:t.dimension})})),E(ho(t.to),(function(t){var e=jG(n.updatedSeries,t);if(e>=0){var i=n.updatedSeries[e].getData();o.push({data:i,divide:ZG(i),dim:t.dimension})}})),r.length>0&&o.length>0&&YG(r,o,i)}(t,i,n,e)}));else{var o=function(t,e){var n=ft(),i=ft(),r=ft();return E(t.oldSeries,(function(e,n){var o=t.oldData[n],a=UG(e),s=XG(a);i.set(s,o),Y(a)&&E(a,(function(t){r.set(t,{data:o,key:s})}))})),E(e.updatedSeries,(function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.getData(),o=UG(t),a=XG(o),s=i.get(a);if(s)n.set(a,{oldSeries:[{divide:ZG(s),data:s}],newSeries:[{divide:ZG(e),data:e}]});else if(Y(o)){var l=[];E(o,(function(t){var e=i.get(t);e&&l.push({divide:ZG(e),data:e})})),l.length&&n.set(a,{oldSeries:l,newSeries:[{data:e,divide:ZG(e)}]})}else{var u=r.get(o);if(u){var h=n.get(u.key);h||(h={oldSeries:[{data:u.data,divide:ZG(u.data)}],newSeries:[]},n.set(u.key,h)),h.newSeries.push({data:e,divide:ZG(e)})}}}})),n}(i,n);E(o.keys(),(function(t){var n=o.get(t);YG(n.oldSeries,n.newSeries,e)}))}E(n.updatedSeries,(function(t){t.__universalTransitionEnabled&&(t.__universalTransitionEnabled=!1)}))}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldData=[],u=0;u3)for(n=[n],i=3;i0?g(v.type,v.props,v.key,null,v.__v):v)){if(v.__=n,v.__b=n.__b+1,null===(h=D[p])||h&&v.key==h.key&&v.type===h.type)D[p]=void 0;else for(f=0;f3;)n.pop()();if(n[1]>>1,1),t.i.removeChild(e)}}),L(v(oe,{context:t.context},e.__v),t.l)):t.l&&t.componentWillUnmount()}(ne.prototype=new y).__e=function(e){var t=this,n=te(t.__v),r=t.o.get(e);return r[0]++,function(o){var i=function(){t.props.revealOrder?(r.push(o),re(t,e,r)):o()};n?n(i):i()}},ne.prototype.render=function(e){this.u=null,this.o=new Map;var t=R(e.children);e.revealOrder&&"b"===e.revealOrder[0]&&t.reverse();for(var n=t.length;n--;)this.o.set(t[n],this.u=[1,0,this.u]);return e.children},ne.prototype.componentDidUpdate=ne.prototype.componentDidMount=function(){var e=this;this.o.forEach((function(t,n){re(e,n,t)}))};var ae="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,se=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,le=function(e){return("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/i:/fil|che|ra/i).test(e)};y.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach((function(e){Object.defineProperty(y.prototype,e,{configurable:!0,get:function(){return this["UNSAFE_"+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})}));var ue=i.event;function ce(){}function de(){return this.cancelBubble}function pe(){return this.defaultPrevented}i.event=function(e){return ue&&(e=ue(e)),e.persist=ce,e.isPropagationStopped=de,e.isDefaultPrevented=pe,e.nativeEvent=e};var fe={configurable:!0,get:function(){return this.class}},he=i.vnode;i.vnode=function(e){var t=e.type,n=e.props,r=n;if("string"==typeof t){for(var o in r={},n){var i=n[o];"value"===o&&"defaultValue"in n&&null==i||("defaultValue"===o&&"value"in n&&null==n.value?o="value":"download"===o&&!0===i?i="":/ondoubleclick/i.test(o)?o="ondblclick":/^onchange(textarea|input)/i.test(o+t)&&!le(n.type)?o="oninput":/^on(Ani|Tra|Tou|BeforeInp)/.test(o)?o=o.toLowerCase():se.test(o)?o=o.replace(/[A-Z0-9]/,"-$&").toLowerCase():null===i&&(i=void 0),r[o]=i)}"select"==t&&r.multiple&&Array.isArray(r.value)&&(r.value=R(n.children).forEach((function(e){e.props.selected=-1!=r.value.indexOf(e.props.value)}))),"select"==t&&null!=r.defaultValue&&(r.value=R(n.children).forEach((function(e){e.props.selected=r.multiple?-1!=r.defaultValue.indexOf(e.props.value):r.defaultValue==e.props.value}))),e.props=r}t&&n.class!=n.className&&(fe.enumerable="className"in n,null!=n.className&&(r.class=n.className),Object.defineProperty(r,"className",fe)),e.$$typeof=ae,he&&he(e)};var ve=i.__r;i.__r=function(e){ve&&ve(e)},"object"==typeof performance&&"function"==typeof performance.now&&performance.now.bind(performance);var ge="undefined"!=typeof globalThis?globalThis:window;ge.FullCalendarVDom?console.warn("FullCalendar VDOM already loaded"):ge.FullCalendarVDom={Component:y,createElement:v,render:L,createRef:function(){return{current:null}},Fragment:m,createContext:function(e){var t=function(e,t){var n={__c:t="__cC"+u++,__:e,Consumer:function(e,t){return e.children(t)},Provider:function(e){var n,r;return this.getChildContext||(n=[],(r={})[t]=this,this.getChildContext=function(){return r},this.shouldComponentUpdate=function(e){this.props.value!==e.value&&n.some(b)},this.sub=function(e){n.push(e);var t=e.componentWillUnmount;e.componentWillUnmount=function(){n.splice(n.indexOf(e),1),t&&t.call(e)}}),e.children}};return n.Provider.__=n.Consumer.contextType=n}(e),n=t.Provider;return t.Provider=function(){var e=this,t=!this.getChildContext,r=n.apply(this,arguments);if(t){var o=[];this.shouldComponentUpdate=function(t){e.props.value!==t.value&&o.forEach((function(e){e.context=t.value,e.forceUpdate()}))},this.sub=function(e){o.push(e);var t=e.componentWillUnmount;e.componentWillUnmount=function(){o.splice(o.indexOf(e),1),t&&t.call(e)}}}return r},t},createPortal:function(e,t){return v(ie,{__v:e,i:t})},flushSync:function(e){e();var t=i.debounceRendering,n=[];function r(e){n.push(e)}i.debounceRendering=r,L(v(me,{}),document.createElement("div"));for(;n.length;)n.shift()();i.debounceRendering=t},unmountComponentAtNode:function(e){L(null,e)}};var me=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){return v("div",{})},t.prototype.componentDidMount=function(){this.setState({})},t}(y);var ye=function(){function e(e,t){this.context=e,this.internalEventSource=t}return e.prototype.remove=function(){this.context.dispatch({type:"REMOVE_EVENT_SOURCE",sourceId:this.internalEventSource.sourceId})},e.prototype.refetch=function(){this.context.dispatch({type:"FETCH_EVENT_SOURCES",sourceIds:[this.internalEventSource.sourceId],isRefetch:!0})},Object.defineProperty(e.prototype,"id",{get:function(){return this.internalEventSource.publicId},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"url",{get:function(){return this.internalEventSource.meta.url},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"format",{get:function(){return this.internalEventSource.meta.format},enumerable:!1,configurable:!0}),e}();function Ee(e){e.parentNode&&e.parentNode.removeChild(e)}function Se(e,t){if(e.closest)return e.closest(t);if(!document.documentElement.contains(e))return null;do{if(be(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}function be(e,t){return(e.matches||e.matchesSelector||e.msMatchesSelector).call(e,t)}function De(e,t){for(var n=e instanceof HTMLElement?[e]:e,r=[],o=0;o=0;i-=1){var a=e[i][r];if("object"==typeof a&&a)o.unshift(a);else if(void 0!==a){n[r]=a;break}}o.length&&(n[r]=Et(o))}for(i=e.length-1;i>=0;i-=1){var s=e[i];for(var l in s)l in n||(n[l]=s[l])}return n}function St(e,t){var n={};for(var r in e)t(e[r],r)&&(n[r]=e[r]);return n}function bt(e,t){var n={};for(var r in e)n[r]=t(e[r],r);return n}function Dt(e){for(var t={},n=0,r=e;n10&&(null==t?r=r.replace("Z",""):0!==t&&(r=r.replace("Z",jt(t,!0)))),r}function Bt(e){return e.toISOString().replace(/T.*$/,"")}function zt(e){return Ze(e.getUTCHours(),2)+":"+Ze(e.getUTCMinutes(),2)+":"+Ze(e.getUTCSeconds(),2)}function jt(e,t){void 0===t&&(t=!1);var n=e<0?"-":"+",r=Math.abs(e),o=Math.floor(r/60),i=Math.round(r%60);return t?n+Ze(o,2)+":"+Ze(i,2):"GMT"+n+o+(i?":"+Ze(i,2):"")}function Gt(e,t,n){if(e===t)return!0;var r,o=e.length;if(o!==t.length)return!1;for(r=0;r1)||"numeric"!==o.year&&"2-digit"!==o.year||"numeric"!==o.month&&"2-digit"!==o.month||"numeric"!==o.day&&"2-digit"!==o.day||(s=1);var l=this.format(e,n),u=this.format(t,n);if(l===u)return l;var c=nn(function(e,t){var n={};for(var r in e)(!(r in Xt)||Xt[r]<=t)&&(n[r]=e[r]);return n}(o,s),i,n),d=c(e),p=c(t),f=function(e,t,n,r){var o=0;for(;o=Ut(t)&&(r=tt(r,1))}return e.start&&(n=lt(e.start),r&&r<=n&&(r=tt(n,1))),{start:n,end:r}}function Vn(e){var t=Wn(e);return ot(t.start,t.end)>1}function Fn(e,t,n,r){return"year"===r?Nt(n.diffWholeYears(e,t),"year"):"month"===r?Nt(n.diffWholeMonths(e,t),"month"):it(e,t)}function Bn(e,t){var n,r,o=[],i=t.start;for(e.sort(zn),n=0;ni&&o.push({start:i,end:r.start}),r.end>i&&(i=r.end);return it.start)&&(null===e.start||null===t.end||e.start=e.start)&&(null===e.end||null!==t.end&&t.end<=e.end)}function Zn(e,t){return(null===e.start||t>=e.start)&&(null===e.end||t=(n||t.end),isToday:t&&Zn(t,r.start)}}function lr(e){var t=["fc-event"];return e.isMirror&&t.push("fc-event-mirror"),e.isDraggable&&t.push("fc-event-draggable"),(e.isStartResizable||e.isEndResizable)&&t.push("fc-event-resizable"),e.isDragging&&t.push("fc-event-dragging"),e.isResizing&&t.push("fc-event-resizing"),e.isSelected&&t.push("fc-event-selected"),e.isStart&&t.push("fc-event-start"),e.isEnd&&t.push("fc-event-end"),e.isPast&&t.push("fc-event-past"),e.isToday&&t.push("fc-event-today"),e.isFuture&&t.push("fc-event-future"),t}function ur(e){return e.instance?e.instance.instanceId:e.def.defId+":"+e.range.start.toISOString()}function cr(e,t){var n=e.eventRange,r=n.def,o=n.instance,i=r.url;if(i)return{href:i};var a=t.emitter,s=t.options.eventInteractive;return null==s&&null==(s=r.interactive)&&(s=Boolean(a.hasHandlers("eventClick"))),s?Oe((function(e){a.trigger("eventClick",{el:e.target,event:new xr(t,r,o),jsEvent:e,view:t.viewApi})})):{}}var dr={start:yn,end:yn,allDay:Boolean};function pr(e,t,n){var o=function(e,t){var n=mn(e,dr),o=n.refined,i=n.extra,a=o.start?t.createMarkerMeta(o.start):null,s=o.end?t.createMarkerMeta(o.end):null,l=o.allDay;null==l&&(l=a&&a.isTimeUnspecified&&(!s||s.isTimeUnspecified));return r({range:{start:a?a.marker:null,end:s?s.marker:null},allDay:l},i)}(e,t),i=o.range;if(!i.start)return null;if(!i.end){if(null==n)return null;i.end=t.add(i.start,n)}return o}function fr(e,t){return Gn(e.range,t.range)&&e.allDay===t.allDay&&function(e,t){for(var n in t)if("range"!==n&&"allDay"!==n&&e[n]!==t[n])return!1;for(var n in e)if(!(n in t))return!1;return!0}(e,t)}function hr(e,t,n){return r(r({},vr(e,t,n)),{timeZone:t.timeZone})}function vr(e,t,n){return{start:t.toDate(e.start),end:t.toDate(e.end),startStr:t.formatIso(e.start,{omitTime:n}),endStr:t.formatIso(e.end,{omitTime:n})}}function gr(e,t,n){var r=On({editable:!1},n),o=Ln(r.refined,r.extra,"",e.allDay,!0,n);return{def:o,ui:er(o,t),instance:mt(o.defId,e.range),range:e.range,isStart:!0,isEnd:!0}}function mr(e,t,n){n.emitter.trigger("select",r(r({},yr(e,n)),{jsEvent:t?t.origEvent:null,view:n.viewApi||n.calendarApi.view}))}function yr(e,t){for(var n,o,i={},a=0,s=t.pluginHooks.dateSpanTransforms;a=0;r-=1){var o=n[r].parseMeta(e);if(o)return{sourceDefId:r,meta:o}}return null}(i,t);if(s)return{_raw:e,isFetching:!1,latestFetchId:"",fetchRange:null,defaultAllDay:i.defaultAllDay,eventDataTransform:i.eventDataTransform,success:i.success,failure:i.failure,publicId:i.id||"",sourceId:Le(),sourceDefId:s.sourceDefId,meta:s.meta,ui:kn(i,t),extendedProps:a}}return null}function _r(e){return r(r(r({},_n),wr),e.pluginHooks.eventSourceRefiners)}function Tr(e,t){return"function"==typeof e&&(e=e()),null==e?t.createNowMarker():t.createMarker(e)}var kr=function(){function e(){}return e.prototype.getCurrentData=function(){return this.currentDataManager.getCurrentData()},e.prototype.dispatch=function(e){return this.currentDataManager.dispatch(e)},Object.defineProperty(e.prototype,"view",{get:function(){return this.getCurrentData().viewApi},enumerable:!1,configurable:!0}),e.prototype.batchRendering=function(e){e()},e.prototype.updateSize=function(){this.trigger("_resize",!0)},e.prototype.setOption=function(e,t){this.dispatch({type:"SET_OPTION",optionName:e,rawOptionValue:t})},e.prototype.getOption=function(e){return this.currentDataManager.currentCalendarOptionsInput[e]},e.prototype.getAvailableLocaleCodes=function(){return Object.keys(this.getCurrentData().availableRawLocales)},e.prototype.on=function(e,t){var n=this.currentDataManager;n.currentCalendarOptionsRefiners[e]?n.emitter.on(e,t):console.warn("Unknown listener name '"+e+"'")},e.prototype.off=function(e,t){this.currentDataManager.emitter.off(e,t)},e.prototype.trigger=function(e){for(var t,n=[],r=1;r=1?Math.min(o,i):o}(e,this.weekDow,this.weekDoy)},e.prototype.format=function(e,t,n){return void 0===n&&(n={}),t.format({marker:e,timeZoneOffset:null!=n.forcedTzo?n.forcedTzo:this.offsetForMarker(e)},this)},e.prototype.formatRange=function(e,t,n,r){return void 0===r&&(r={}),r.isEndExclusive&&(t=nt(t,-1)),n.formatRange({marker:e,timeZoneOffset:null!=r.forcedStartTzo?r.forcedStartTzo:this.offsetForMarker(e)},{marker:t,timeZoneOffset:null!=r.forcedEndTzo?r.forcedEndTzo:this.offsetForMarker(t)},this,r.defaultSeparator)},e.prototype.formatIso=function(e,t){void 0===t&&(t={});var n=null;return t.omitTimeZoneOffset||(n=null!=t.forcedTzo?t.forcedTzo:this.offsetForMarker(e)),Ft(e,n,t.omitTime)},e.prototype.timestampToMarker=function(e){return"local"===this.timeZone?ht(dt(new Date(e))):"UTC"!==this.timeZone&&this.namedTimeZoneImpl?ht(this.namedTimeZoneImpl.timestampToArray(e)):new Date(e)},e.prototype.offsetForMarker=function(e){return"local"===this.timeZone?-pt(ft(e)).getTimezoneOffset():"UTC"===this.timeZone?0:this.namedTimeZoneImpl?this.namedTimeZoneImpl.offsetForArray(ft(e)):null},e.prototype.toDate=function(e,t){return"local"===this.timeZone?pt(ft(e)):"UTC"===this.timeZone?new Date(e.valueOf()):this.namedTimeZoneImpl?new Date(e.valueOf()-1e3*this.namedTimeZoneImpl.offsetForArray(ft(e))*60):new Date(e.valueOf()-(t||0))},e}(),Ur=[],Wr={code:"en",week:{dow:0,doy:4},direction:"ltr",buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"year",today:"today",month:"month",week:"week",day:"day",list:"list"},weekText:"W",weekTextLong:"Week",closeHint:"Close",timeHint:"Time",eventHint:"Event",allDayText:"all-day",moreLinkText:"more",noEventsText:"No events to display"},Vr=r(r({},Wr),{buttonHints:{prev:"Previous $0",next:"Next $0",today:function(e,t){return"day"===t?"Today":"This "+e}},viewHint:"$0 view",navLinkHint:"Go to $0",moreLinkHint:function(e){return"Show "+e+" more event"+(1===e?"":"s")}});function Fr(e){for(var t=e.length>0?e[0].code:"en",n=Ur.concat(e),r={en:Vr},o=0,i=n;o0;o-=1){var i=r.slice(0,o).join("-");if(t[i])return t[i]}return null}(n,t)||Vr;return zr(e,n,r)}(e,t):zr(e.code,[e.code],e)}function zr(e,t,n){var r=Et([Wr,n],["buttonText"]);delete r.code;var o=r.week;return delete r.week,{codeArg:e,codes:t,week:o,simpleNumberFormat:new Intl.NumberFormat(e),options:r}}function jr(e){var t=Br(e.locale||"en",Fr([]).map);return new Lr(r(r({timeZone:cn.timeZone,calendarSystem:"gregory"},e),{locale:t}))}var Gr,qr={startTime:"09:00",endTime:"17:00",daysOfWeek:[1,2,3,4,5],display:"inverse-background",classNames:"fc-non-business",groupId:"_businessHours"};function Yr(e,t){return En(function(e){var t;t=!0===e?[{}]:Array.isArray(e)?e.filter((function(e){return e.daysOfWeek})):"object"==typeof e&&e?[e]:[];return t=t.map((function(e){return r(r({},qr),e)}))}(e),null,t)}function Zr(e,t){return e.left>=t.left&&e.left=t.top&&e.top
",e.querySelector("table").style.height="100px",e.querySelector("div").style.height="100%",document.body.appendChild(e);var t=e.querySelector("div").offsetHeight>0;return document.body.removeChild(e),t}()),Gr}var eo={defs:{},instances:{}},to=function(){function e(){this.getKeysForEventDefs=qt(this._getKeysForEventDefs),this.splitDateSelection=qt(this._splitDateSpan),this.splitEventStore=qt(this._splitEventStore),this.splitIndividualUi=qt(this._splitIndividualUi),this.splitEventDrag=qt(this._splitInteraction),this.splitEventResize=qt(this._splitInteraction),this.eventUiBuilders={}}return e.prototype.splitProps=function(e){var t=this,n=this.getKeyInfo(e),r=this.getKeysForEventDefs(e.eventStore),o=this.splitDateSelection(e.dateSelection),i=this.splitIndividualUi(e.eventUiBases,r),a=this.splitEventStore(e.eventStore,r),s=this.splitEventDrag(e.eventDrag),l=this.splitEventResize(e.eventResize),u={};for(var c in this.eventUiBuilders=bt(n,(function(e,n){return t.eventUiBuilders[n]||qt(no)})),n){var d=n[c],p=a[c]||eo,f=this.eventUiBuilders[c];u[c]={businessHours:d.businessHours||e.businessHours,dateSelection:o[c]||null,eventStore:p,eventUiBases:f(e.eventUiBases[""],d.ui,i[c]),eventSelection:p.instances[e.eventSelection]?e.eventSelection:"",eventDrag:s[c]||null,eventResize:l[c]||null}}return u},e.prototype._splitDateSpan=function(e){var t={};if(e)for(var n=0,r=this.getKeysForDateSpan(e);nn:!!t&&e>=t.end)}}function oo(e,t){var n=["fc-day","fc-day-"+Qe[e.dow]];return e.isDisabled?n.push("fc-day-disabled"):(e.isToday&&(n.push("fc-day-today"),n.push(t.getClass("today"))),e.isPast&&n.push("fc-day-past"),e.isFuture&&n.push("fc-day-future"),e.isOther&&n.push("fc-day-other")),n}var io=ln({year:"numeric",month:"long",day:"numeric"}),ao=ln({week:"long"});function so(e,t,n,o){void 0===n&&(n="day"),void 0===o&&(o=!0);var i=e.dateEnv,a=e.options,s=e.calendarApi,l=i.format(t,"week"===n?ao:io);if(a.navLinks){var u=i.toDate(t),c=function(e){var r="day"===n?a.navLinkDayClick:"week"===n?a.navLinkWeekClick:null;"function"==typeof r?r.call(s,i.toDate(t),e):("string"==typeof r&&(n=r),s.zoomTo(t,n))};return r({title:Xe(a.navLinkHint,[l,u],l),"data-navlink":""},o?He(c):{onClick:c})}return{"aria-label":l}}var lo,uo=null;function co(){return null===uo&&(uo=function(){var e=document.createElement("div");we(e,{position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}),e.innerHTML="
",document.body.appendChild(e);var t=e.firstChild.getBoundingClientRect().left>e.getBoundingClientRect().left;return Ee(e),t}()),uo}function po(){return lo||(lo=function(){var e=document.createElement("div");e.style.overflow="scroll",e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",document.body.appendChild(e);var t=fo(e);return document.body.removeChild(e),t}()),lo}function fo(e){return{x:e.offsetHeight-e.clientHeight,y:e.offsetWidth-e.clientWidth}}function ho(e,t){void 0===t&&(t=!1);var n=window.getComputedStyle(e),r=parseInt(n.borderLeftWidth,10)||0,o=parseInt(n.borderRightWidth,10)||0,i=parseInt(n.borderTopWidth,10)||0,a=parseInt(n.borderBottomWidth,10)||0,s=fo(e),l=s.y-r-o,u={borderLeft:r,borderRight:o,borderTop:i,borderBottom:a,scrollbarBottom:s.x-i-a,scrollbarLeft:0,scrollbarRight:0};return co()&&"rtl"===n.direction?u.scrollbarLeft=l:u.scrollbarRight=l,t&&(u.paddingLeft=parseInt(n.paddingLeft,10)||0,u.paddingRight=parseInt(n.paddingRight,10)||0,u.paddingTop=parseInt(n.paddingTop,10)||0,u.paddingBottom=parseInt(n.paddingBottom,10)||0),u}function vo(e,t,n){void 0===t&&(t=!1);var r=n?e.getBoundingClientRect():go(e),o=ho(e,t),i={left:r.left+o.borderLeft+o.scrollbarLeft,right:r.right-o.borderRight-o.scrollbarRight,top:r.top+o.borderTop,bottom:r.bottom-o.borderBottom-o.scrollbarBottom};return t&&(i.left+=o.paddingLeft,i.right-=o.paddingRight,i.top+=o.paddingTop,i.bottom-=o.paddingBottom),i}function go(e){var t=e.getBoundingClientRect();return{left:t.left+window.pageXOffset,top:t.top+window.pageYOffset,right:t.right+window.pageXOffset,bottom:t.bottom+window.pageYOffset}}function mo(e){for(var t=[];e instanceof HTMLElement;){var n=window.getComputedStyle(e);if("fixed"===n.position)break;/(auto|scroll)/.test(n.overflow+n.overflowY+n.overflowX)&&t.push(e),e=e.parentNode}return t}function yo(e,t,n){var r=!1,o=function(){r||(r=!0,t.apply(this,arguments))},i=function(){r||(r=!0,n&&n.apply(this,arguments))},a=e(o,i);a&&"function"==typeof a.then&&a.then(o,i)}var Eo=function(){function e(){this.handlers={},this.thisContext=null}return e.prototype.setThisContext=function(e){this.thisContext=e},e.prototype.setOptions=function(e){this.options=e},e.prototype.on=function(e,t){!function(e,t,n){(e[t]||(e[t]=[])).push(n)}(this.handlers,e,t)},e.prototype.off=function(e,t){!function(e,t,n){n?e[t]&&(e[t]=e[t].filter((function(e){return e!==n}))):delete e[t]}(this.handlers,e,t)},e.prototype.trigger=function(e){for(var t=[],n=1;n=n[t]&&e=n[t]&&e0},e.prototype.canScrollHorizontally=function(){return this.getMaxScrollLeft()>0},e.prototype.canScrollUp=function(){return this.getScrollTop()>0},e.prototype.canScrollDown=function(){return this.getScrollTop()0},e.prototype.canScrollRight=function(){return this.getScrollLeft()=c.end?new Date(c.end.valueOf()-1):u),o=this.buildCurrentRangeInfo(e,t),i=/^(year|month|week|day)$/.test(o.unit),a=this.buildRenderRange(this.trimHiddenDays(o.range),o.unit,i),s=a=this.trimHiddenDays(a),d.showNonCurrentDates||(s=jn(s,o.range)),s=jn(s=this.adjustActiveRange(s),r),l=qn(o.range,r),{validRange:r,currentRange:o.range,currentRangeUnit:o.unit,isRangeAllDay:i,activeRange:s,renderRange:a,slotMinTime:d.slotMinTime,slotMaxTime:d.slotMaxTime,isValid:l,dateIncrement:this.buildDateIncrement(o.duration)}},e.prototype.buildValidRange=function(){var e=this.props.validRangeInput,t="function"==typeof e?e.call(this.props.calendarApi,this.nowDate):e;return this.refineRange(t)||{start:null,end:null}},e.prototype.buildCurrentRangeInfo=function(e,t){var n,r=this.props,o=null,i=null,a=null;return r.duration?(o=r.duration,i=r.durationUnit,a=this.buildRangeFromDuration(e,t,o,i)):(n=this.props.dayCount)?(i="day",a=this.buildRangeFromDayCount(e,t,n)):(a=this.buildCustomVisibleRange(e))?i=r.dateEnv.greatestWholeUnit(a.start,a.end).unit:(i=Vt(o=this.getFallbackDuration()).unit,a=this.buildRangeFromDuration(e,t,o,i)),{duration:o,unit:i,range:a}},e.prototype.getFallbackDuration=function(){return Nt({day:1})},e.prototype.adjustActiveRange=function(e){var t=this.props,n=t.dateEnv,r=t.usesMinMaxTime,o=t.slotMinTime,i=t.slotMaxTime,a=e.start,s=e.end;return r&&(Lt(o)<0&&(a=lt(a),a=n.add(a,o)),Lt(i)>1&&(s=tt(s=lt(s),-1),s=n.add(s,i))),{start:a,end:s}},e.prototype.buildRangeFromDuration=function(e,t,n,r){var o,i,a,s=this.props,l=s.dateEnv,u=s.dateAlignment;if(!u){var c=this.props.dateIncrement;u=c&&Ut(c)e.fetchRange.end}(e,t,n)})),t,!1,n)}function pi(e,t,n,r,o){var i={};for(var a in e){var s=e[a];t[a]?i[a]=fi(s,n,r,o):i[a]=s}return i}function fi(e,t,n,o){var i=o.options,a=o.calendarApi,s=o.pluginHooks.eventSourceDefs[e.sourceDefId],l=Le();return s.fetch({eventSource:e,range:t,isRefetch:n,context:o},(function(n){var r=n.rawEvents;i.eventSourceSuccess&&(r=i.eventSourceSuccess.call(a,r,n.xhr)||r),e.success&&(r=e.success.call(a,r,n.xhr)||r),o.dispatch({type:"RECEIVE_EVENTS",sourceId:e.sourceId,fetchId:l,fetchRange:t,rawEvents:r})}),(function(n){console.warn(n.message,n),i.eventSourceFailure&&i.eventSourceFailure.call(a,n),e.failure&&e.failure(n),o.dispatch({type:"RECEIVE_EVENT_ERROR",sourceId:e.sourceId,fetchId:l,fetchRange:t,error:n})})),r(r({},e),{isFetching:!0,latestFetchId:l})}function hi(e,t){return St(e,(function(e){return vi(e,t)}))}function vi(e,t){return!t.pluginHooks.eventSourceDefs[e.sourceDefId].ignoreRange}function gi(e,t,n,r,o){switch(t.type){case"RECEIVE_EVENTS":return function(e,t,n,r,o,i){if(t&&n===t.latestFetchId){var a=En(function(e,t,n){var r=n.options.eventDataTransform,o=t?t.eventDataTransform:null;o&&(e=mi(e,o));r&&(e=mi(e,r));return e}(o,t,i),t,i);return r&&(a=xt(a,r,i)),Cn(yi(e,t.sourceId),a)}return e}(e,n[t.sourceId],t.fetchId,t.fetchRange,t.rawEvents,o);case"ADD_EVENTS":return function(e,t,n,r){n&&(t=xt(t,n,r));return Cn(e,t)}(e,t.eventStore,r?r.activeRange:null,o);case"RESET_EVENTS":return t.eventStore;case"MERGE_EVENTS":return Cn(e,t.eventStore);case"PREV":case"NEXT":case"CHANGE_DATE":case"CHANGE_VIEW_TYPE":return r?xt(e,r.activeRange,o):e;case"REMOVE_EVENTS":return function(e,t){var n=e.defs,r=e.instances,o={},i={};for(var a in n)t.defs[a]||(o[a]=n[a]);for(var s in r)!t.instances[s]&&o[r[s].defId]&&(i[s]=r[s]);return{defs:o,instances:i}}(e,t.eventStore);case"REMOVE_EVENT_SOURCE":return yi(e,t.sourceId);case"REMOVE_ALL_EVENT_SOURCES":return wn(e,(function(e){return!e.sourceId}));case"REMOVE_ALL_EVENTS":return{defs:{},instances:{}};default:return e}}function mi(e,t){var n;if(t){n=[];for(var r=0,o=e;r=200&&a.status<400){var e=!1,t=void 0;try{t=JSON.parse(a.responseText),e=!0}catch(e){}e?r(t,a):o("Failure parsing JSON",a)}else o("Request failed",a)},a.onerror=function(){o("Request failed",a)},a.send(i)}function Ti(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t.join("&")}function ki(e,t){for(var n=Ct(t.getCurrentData().eventSources),r=[],o=0,i=e;o1)return{year:"numeric",month:"short",day:"numeric"};return{year:"numeric",month:"long",day:"numeric"}}(e)),{isEndExclusive:e.isRangeAllDay,defaultSeparator:t.titleRangeSeparator})}var Ni=function(){function e(e){var t=this;this.computeOptionsData=qt(this._computeOptionsData),this.computeCurrentViewData=qt(this._computeCurrentViewData),this.organizeRawLocales=qt(Fr),this.buildLocale=qt(Br),this.buildPluginHooks=jo(),this.buildDateEnv=qt(Hi),this.buildTheme=qt(Oi),this.parseToolbars=qt(Ci),this.buildViewSpecs=qt(oi),this.buildDateProfileGenerator=Yt(Ai),this.buildViewApi=qt(Li),this.buildViewUiProps=Yt(Vi),this.buildEventUiBySource=qt(Ui,wt),this.buildEventUiBases=qt(Wi),this.parseContextBusinessHours=Yt(Bi),this.buildTitle=qt(Pi),this.emitter=new Eo,this.actionRunner=new Ii(this._handleAction.bind(this),this.updateData.bind(this)),this.currentCalendarOptionsInput={},this.currentCalendarOptionsRefined={},this.currentViewOptionsInput={},this.currentViewOptionsRefined={},this.currentCalendarOptionsRefiners={},this.getCurrentData=function(){return t.data},this.dispatch=function(e){t.actionRunner.request(e)},this.props=e,this.actionRunner.pause();var n={},o=this.computeOptionsData(e.optionOverrides,n,e.calendarApi),i=o.calendarOptions.initialView||o.pluginHooks.initialView,a=this.computeCurrentViewData(i,o,e.optionOverrides,n);e.calendarApi.currentDataManager=this,this.emitter.setThisContext(e.calendarApi),this.emitter.setOptions(a.options);var s,l,u,c=(s=o.calendarOptions,l=o.dateEnv,null!=(u=s.initialDate)?l.createMarker(u):Tr(s.now,l)),d=a.dateProfileGenerator.build(c);Zn(d.activeRange,c)||(c=d.currentRange.start);for(var p={dateEnv:o.dateEnv,options:o.calendarOptions,pluginHooks:o.pluginHooks,calendarApi:e.calendarApi,dispatch:this.dispatch,emitter:this.emitter,getCurrentData:this.getCurrentData},f=0,h=o.pluginHooks.contextInit;fs.end&&(r+=this.insertEntry({index:e.index,thickness:e.thickness,span:{start:s.end,end:a.end}},i)),r?(n.push.apply(n,o([{index:e.index,thickness:e.thickness,span:$i(s,a)}],i)),r):(n.push(e),0)},e.prototype.insertEntryAt=function(e,t){var n=this.entriesByLevel,r=this.levelCoords;-1===t.lateral?(Ji(r,t.level,t.levelCoord),Ji(n,t.level,[e])):Ji(n[t.level],t.lateral,e),this.stackCnts[Zi(e)]=t.stackCnt},e.prototype.findInsertion=function(e){for(var t=this,n=t.levelCoords,r=t.entriesByLevel,o=t.strictOrder,i=t.stackCnts,a=n.length,s=0,l=-1,u=-1,c=null,d=0,p=0;p=s+e.thickness)break;for(var h=r[p],v=void 0,g=Qi(h,e.span.start,Yi),m=g[0]+g[1];(v=h[m])&&v.span.starts&&(s=y,c=v,l=p,u=m),y===s&&(d=Math.max(d,i[Zi(v)]+1)),m+=1}}var E=0;if(c)for(E=l+1;En(e[o-1]))return[o,0];for(;ra))return[i,1];r=i+1}}return[r,0]}var ea=function(){function e(e){this.component=e.component,this.isHitComboAllowed=e.isHitComboAllowed||null}return e.prototype.destroy=function(){},e}();function ta(e,t){return{component:e,el:t.el,useEventCenter:null==t.useEventCenter||t.useEventCenter,isHitComboAllowed:t.isHitComboAllowed||null}}function na(e){var t;return(t={})[e.component.uid]=e,t}var ra={},oa=function(){function e(e,t){this.emitter=new Eo}return e.prototype.destroy=function(){},e.prototype.setMirrorIsVisible=function(e){},e.prototype.setMirrorNeedsRevert=function(e){},e.prototype.setAutoScrollEnabled=function(e){},e}(),ia={},aa={startTime:Nt,duration:Nt,create:Boolean,sourceId:String};function sa(e){var t=mn(e,aa),n=t.refined,r=t.extra;return{startTime:n.startTime||null,duration:n.duration||null,create:null==n.create||n.create,sourceId:n.sourceId,leftoverProps:r}}var la=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){var e=this,t=this.props.widgetGroups.map((function(t){return e.renderWidgetGroup(t)}));return _o.apply(void 0,o(["div",{className:"fc-toolbar-chunk"}],t))},t.prototype.renderWidgetGroup=function(e){for(var t=this.props,n=this.context.theme,r=[],i=!0,a=0,s=e;a1){var m=i&&n.getClass("buttonGroup")||"";return _o.apply(void 0,o(["div",{className:m}],r))}return r[0]},t}(Uo),ua=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){var e,t,n=this.props,r=n.model,o=n.extraClassName,i=!1,a=r.sectionWidgets,s=a.center;return a.left?(i=!0,e=a.left):e=a.start,a.right?(i=!0,t=a.right):t=a.end,_o("div",{className:[o||"","fc-toolbar",i?"fc-toolbar-ltr":""].join(" ")},this.renderSection("start",e||[]),this.renderSection("center",s||[]),this.renderSection("end",t||[]))},t.prototype.renderSection=function(e,t){var n=this.props;return _o(la,{key:e,widgetGroups:t,title:n.title,navUnit:n.navUnit,activeButton:n.activeButton,isTodayEnabled:n.isTodayEnabled,isPrevEnabled:n.isPrevEnabled,isNextEnabled:n.isNextEnabled,titleId:n.titleId})},t}(Uo),ca=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={availableWidth:null},t.handleEl=function(e){t.el=e,Fo(t.props.elRef,e),t.updateAvailableWidth()},t.handleResize=function(){t.updateAvailableWidth()},t}return n(t,e),t.prototype.render=function(){var e=this.props,t=this.state,n=e.aspectRatio,r=["fc-view-harness",n||e.liquid||e.height?"fc-view-harness-active":"fc-view-harness-passive"],o="",i="";return n?null!==t.availableWidth?o=t.availableWidth/n:i=1/n*100+"%":o=e.height||"",_o("div",{"aria-labelledby":e.labeledById,ref:this.handleEl,className:r.join(" "),style:{height:o,paddingBottom:i}},e.children)},t.prototype.componentDidMount=function(){this.context.addResizeHandler(this.handleResize)},t.prototype.componentWillUnmount=function(){this.context.removeResizeHandler(this.handleResize)},t.prototype.updateAvailableWidth=function(){this.el&&this.props.aspectRatio&&this.setState({availableWidth:this.el.offsetWidth})},t}(Uo),da=function(e){function t(t){var n=e.call(this,t)||this;return n.handleSegClick=function(e,t){var r=n.component,o=r.context,i=Jn(t);if(i&&r.isValidSegDownEl(e.target)){var a=Se(e.target,".fc-event-forced-url"),s=a?a.querySelector("a[href]").href:"";o.emitter.trigger("eventClick",{el:t,event:new xr(r.context,i.eventRange.def,i.eventRange.instance),jsEvent:e,view:o.viewApi}),s&&!e.defaultPrevented&&(window.location.href=s)}},n.destroy=Ie(t.el,"click",".fc-event",n.handleSegClick),n}return n(t,e),t}(ea),pa=function(e){function t(t){var n,r,o,i,a,s=e.call(this,t)||this;return s.handleEventElRemove=function(e){e===s.currentSegEl&&s.handleSegLeave(null,s.currentSegEl)},s.handleSegEnter=function(e,t){Jn(t)&&(s.currentSegEl=t,s.triggerEvent("eventMouseEnter",e,t))},s.handleSegLeave=function(e,t){s.currentSegEl&&(s.currentSegEl=null,s.triggerEvent("eventMouseLeave",e,t))},s.removeHoverListeners=(n=t.el,r=".fc-event",o=s.handleSegEnter,i=s.handleSegLeave,Ie(n,"mouseover",r,(function(e,t){if(t!==a){a=t,o(e,t);var n=function(e){a=null,i(e,t),t.removeEventListener("mouseleave",n)};t.addEventListener("mouseleave",n)}}))),s}return n(t,e),t.prototype.destroy=function(){this.removeHoverListeners()},t.prototype.triggerEvent=function(e,t,n){var r=this.component,o=r.context,i=Jn(n);t&&!r.isValidSegDownEl(t.target)||o.emitter.trigger(e,{el:n,event:new xr(o,i.eventRange.def,i.eventRange.instance),jsEvent:t,view:o.viewApi})},t}(ea),fa=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.buildViewContext=qt(Ao),t.buildViewPropTransformers=qt(va),t.buildToolbarProps=qt(ha),t.headerRef=ko(),t.footerRef=ko(),t.interactionsStore={},t.state={viewLabelId:xe()},t.registerInteractiveComponent=function(e,n){var r=ta(e,n),o=[da,pa].concat(t.props.pluginHooks.componentInteractions).map((function(e){return new e(r)}));t.interactionsStore[e.uid]=o,ra[e.uid]=r},t.unregisterInteractiveComponent=function(e){var n=t.interactionsStore[e.uid];if(n){for(var r=0,o=n;r10?{weekday:"short"}:t>1?{weekday:"short",month:"numeric",day:"numeric",omitCommas:!0}:{weekday:"long"})}var ya="fc-col-header-cell";function Ea(e){return e.text}var Sa=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){var e=this.context,t=e.dateEnv,n=e.options,o=e.theme,i=e.viewApi,a=this.props,s=a.date,l=a.dateProfile,u=ro(s,a.todayRange,null,l),c=[ya].concat(oo(u,o)),d=t.format(s,a.dayHeaderFormat),p=!u.isDisabled&&a.colCnt>1?so(this.context,s):{},f=r(r(r({date:t.toDate(s),view:i},a.extraHookProps),{text:d}),u);return _o(Yo,{hookProps:f,classNames:n.dayHeaderClassNames,content:n.dayHeaderContent,defaultContent:Ea,didMount:n.dayHeaderDidMount,willUnmount:n.dayHeaderWillUnmount},(function(e,t,n,o){return _o("th",r({ref:e,role:"columnheader",className:c.concat(t).join(" "),"data-date":u.isDisabled?void 0:Bt(s),colSpan:a.colSpan},a.extraDataAttrs),_o("div",{className:"fc-scrollgrid-sync-inner"},!u.isDisabled&&_o("a",r({ref:n,className:["fc-col-header-cell-cushion",a.isSticky?"fc-sticky":""].join(" ")},p),o)))}))},t}(Uo),ba=ln({weekday:"long"}),Da=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){var e=this.props,t=this.context,n=t.dateEnv,o=t.theme,i=t.viewApi,a=t.options,s=tt(new Date(2592e5),e.dow),l={dow:e.dow,isDisabled:!1,isFuture:!1,isPast:!1,isToday:!1,isOther:!1},u=[ya].concat(oo(l,o),e.extraClassNames||[]),c=n.format(s,e.dayHeaderFormat),d=r(r(r(r({date:s},l),{view:i}),e.extraHookProps),{text:c});return _o(Yo,{hookProps:d,classNames:a.dayHeaderClassNames,content:a.dayHeaderContent,defaultContent:Ea,didMount:a.dayHeaderDidMount,willUnmount:a.dayHeaderWillUnmount},(function(t,o,i,a){return _o("th",r({ref:t,role:"columnheader",className:u.concat(o).join(" "),colSpan:e.colSpan},e.extraDataAttrs),_o("div",{className:"fc-scrollgrid-sync-inner"},_o("a",{"aria-label":n.format(s,ba),className:["fc-col-header-cell-cushion",e.isSticky?"fc-sticky":""].join(" "),ref:i},a)))}))},t}(Uo),Ca=function(e){function t(t,n){var r=e.call(this,t,n)||this;return r.initialNowDate=Tr(n.options.now,n.dateEnv),r.initialNowQueriedMs=(new Date).valueOf(),r.state=r.computeTiming().currentState,r}return n(t,e),t.prototype.render=function(){var e=this.props,t=this.state;return e.children(t.nowDate,t.todayRange)},t.prototype.componentDidMount=function(){this.setTimeout()},t.prototype.componentDidUpdate=function(e){e.unit!==this.props.unit&&(this.clearTimeout(),this.setTimeout())},t.prototype.componentWillUnmount=function(){this.clearTimeout()},t.prototype.computeTiming=function(){var e=this.props,t=this.context,n=nt(this.initialNowDate,(new Date).valueOf()-this.initialNowQueriedMs),r=t.dateEnv.startOf(n,e.unit),o=t.dateEnv.add(r,Nt(1,e.unit)),i=o.valueOf()-n.valueOf();return i=Math.min(864e5,i),{currentState:{nowDate:r,todayRange:wa(r)},nextState:{nowDate:o,todayRange:wa(o)},waitMs:i}},t.prototype.setTimeout=function(){var e=this,t=this.computeTiming(),n=t.nextState,r=t.waitMs;this.timeoutId=setTimeout((function(){e.setState(n,(function(){e.setTimeout()}))}),r)},t.prototype.clearTimeout=function(){this.timeoutId&&clearTimeout(this.timeoutId)},t.contextType=Oo,t}(Ro);function wa(e){var t=lt(e);return{start:t,end:tt(t,1)}}var Ra=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.createDayHeaderFormatter=qt(_a),t}return n(t,e),t.prototype.render=function(){var e=this.context,t=this.props,n=t.dates,r=t.dateProfile,o=t.datesRepDistinctDays,i=t.renderIntro,a=this.createDayHeaderFormatter(e.options.dayHeaderFormat,o,n.length);return _o(Ca,{unit:"day"},(function(e,t){return _o("tr",{role:"row"},i&&i("day"),n.map((function(e){return o?_o(Sa,{key:e.toISOString(),date:e,dateProfile:r,todayRange:t,colCnt:n.length,dayHeaderFormat:a}):_o(Da,{key:e.getUTCDay(),dow:e.getUTCDay(),dayHeaderFormat:a})})))}))},t}(Uo);function _a(e,t,n){return e||ma(t,n)}var Ta=function(){function e(e,t){for(var n=e.start,r=e.end,o=[],i=[],a=-1;n=t.length?t[t.length-1]+1:t[n]},e}(),ka=function(){function e(e,t){var n,r,o,i=e.dates;if(t){for(r=i[0].getUTCDay(),n=1;nt)return!0}return!1},t.prototype.needsYScrolling=function(){if(Wa.test(this.props.overflowY))return!1;for(var e=this.el,t=this.el.getBoundingClientRect().height-this.getXScrollbarWidth(),n=e.children,r=0;rt)return!0}return!1},t.prototype.getXScrollbarWidth=function(){return Wa.test(this.props.overflowX)?0:this.el.offsetHeight-this.el.clientHeight},t.prototype.getYScrollbarWidth=function(){return Wa.test(this.props.overflowY)?0:this.el.offsetWidth-this.el.clientWidth},t}(Uo),Fa=function(){function e(e){var t=this;this.masterCallback=e,this.currentMap={},this.depths={},this.callbackMap={},this.handleValue=function(e,n){var r=t,o=r.depths,i=r.currentMap,a=!1,s=!1;null!==e?(a=n in i,i[n]=e,o[n]=(o[n]||0)+1,s=!0):(o[n]-=1,o[n]||(delete i[n],delete t.callbackMap[n],a=!0)),t.masterCallback&&(a&&t.masterCallback(null,String(n)),s&&t.masterCallback(e,String(n)))}}return e.prototype.createRef=function(e){var t=this,n=this.callbackMap[e];return n||(n=this.callbackMap[e]=function(n){t.handleValue(n,String(e))}),n},e.prototype.collect=function(e,t,n){return kt(this.currentMap,e,t,n)},e.prototype.getAll=function(){return Ct(this.currentMap)},e}();function Ba(e){for(var t=0,n=0,r=De(e,".fc-scrollgrid-shrink");n=0&&e=0&&tt.eventRange.range.end?e:t}var Cs=function(e){function t(t,n){void 0===n&&(n={});var o=e.call(this)||this;return o.isRendering=!1,o.isRendered=!1,o.currentClassNames=[],o.customContentRenderId=0,o.handleAction=function(e){switch(e.type){case"SET_EVENT_DRAG":case"SET_EVENT_RESIZE":o.renderRunner.tryDrain()}},o.handleData=function(e){o.currentData=e,o.renderRunner.request(e.calendarOptions.rerenderDelay)},o.handleRenderRequest=function(){if(o.isRendering){o.isRendered=!0;var e=o.currentData;Po((function(){To(_o(ga,{options:e.calendarOptions,theme:e.theme,emitter:e.emitter},(function(t,n,i,a){return o.setClassNames(t),o.setHeight(n),_o(Zo.Provider,{value:o.customContentRenderId},_o(fa,r({isHeightAuto:i,forPrint:a},e)))})),o.el)}))}else o.isRendered&&(o.isRendered=!1,No(o.el),o.setClassNames([]),o.setHeight(""))},o.el=t,o.renderRunner=new Mi(o.handleRenderRequest),new Ni({optionOverrides:n,calendarApi:o,onAction:o.handleAction,onData:o.handleData}),o}return n(t,e),Object.defineProperty(t.prototype,"view",{get:function(){return this.currentData.viewApi},enumerable:!1,configurable:!0}),t.prototype.render=function(){var e=this.isRendering;e?this.customContentRenderId+=1:this.isRendering=!0,this.renderRunner.request(),e&&this.updateSize()},t.prototype.destroy=function(){this.isRendering&&(this.isRendering=!1,this.renderRunner.request())},t.prototype.updateSize=function(){var t=this;Po((function(){e.prototype.updateSize.call(t)}))},t.prototype.batchRendering=function(e){this.renderRunner.pause("batchRendering"),e(),this.renderRunner.resume("batchRendering")},t.prototype.pauseRendering=function(){this.renderRunner.pause("pauseRendering")},t.prototype.resumeRendering=function(){this.renderRunner.resume("pauseRendering",!0)},t.prototype.resetOptions=function(e,t){this.currentDataManager.resetOptions(e,t)},t.prototype.setClassNames=function(e){if(!Gt(e,this.currentClassNames)){for(var t=this.el.classList,n=0,r=this.currentClassNames;n0&&(this.everMovedDown=!0),i<0?this.everMovedLeft=!0:i>0&&(this.everMovedRight=!0),this.pointerScreenX=n,this.pointerScreenY=r,this.isAnimating||(this.isAnimating=!0,this.requestAnimation(Ns()))}},e.prototype.stop=function(){if(this.isEnabled){this.isAnimating=!1;for(var e=0,t=this.scrollCaches;e=0&&u>=0&&c>=0&&d>=0&&(c<=n&&this.everMovedUp&&a.canScrollUp()&&(!r||r.distance>c)&&(r={scrollCache:a,name:"top",distance:c}),d<=n&&this.everMovedDown&&a.canScrollDown()&&(!r||r.distance>d)&&(r={scrollCache:a,name:"bottom",distance:d}),l<=n&&this.everMovedLeft&&a.canScrollLeft()&&(!r||r.distance>l)&&(r={scrollCache:a,name:"left",distance:l}),u<=n&&this.everMovedRight&&a.canScrollRight()&&(!r||r.distance>u)&&(r={scrollCache:a,name:"right",distance:u}))}return r},e.prototype.buildCaches=function(e){return this.queryScrollEls(e).map((function(e){return e===window?new Ps(!1):new Is(e,!1)}))},e.prototype.queryScrollEls=function(e){for(var t=[],n=0,r=this.scrollQuery;n=t*t&&r.handleDistanceSurpassed(e)}r.isDragging&&("scroll"!==e.origEvent.type&&(r.mirror.handleMove(e.pageX,e.pageY),r.autoScroller.handleMove(e.pageX,e.pageY)),r.emitter.trigger("dragmove",e))}},r.onPointerUp=function(e){r.isInteracting&&(r.isInteracting=!1,Fe(document.body),ze(document.body),r.emitter.trigger("pointerup",e),r.isDragging&&(r.autoScroller.stop(),r.tryStopDrag(e)),r.delayTimeoutId&&(clearTimeout(r.delayTimeoutId),r.delayTimeoutId=null))};var o=r.pointer=new Ts(t);return o.emitter.on("pointerdown",r.onPointerDown),o.emitter.on("pointermove",r.onPointerMove),o.emitter.on("pointerup",r.onPointerUp),n&&(o.selector=n),r.mirror=new xs,r.autoScroller=new Hs,r}return n(t,e),t.prototype.destroy=function(){this.pointer.destroy(),this.onPointerUp({})},t.prototype.startDelay=function(e){var t=this;"number"==typeof this.delay?this.delayTimeoutId=setTimeout((function(){t.delayTimeoutId=null,t.handleDelayEnd(e)}),this.delay):this.handleDelayEnd(e)},t.prototype.handleDelayEnd=function(e){this.isDelayEnded=!0,this.tryStartDrag(e)},t.prototype.handleDistanceSurpassed=function(e){this.isDistanceSurpassed=!0,this.tryStartDrag(e)},t.prototype.tryStartDrag=function(e){this.isDelayEnded&&this.isDistanceSurpassed&&(this.pointer.wasTouchScroll&&!this.touchScrollAllowed||(this.isDragging=!0,this.mirrorNeedsRevert=!1,this.autoScroller.start(e.pageX,e.pageY,this.containerEl),this.emitter.trigger("dragstart",e),!1===this.touchScrollAllowed&&this.pointer.cancelTouchScroll()))},t.prototype.tryStopDrag=function(e){this.mirror.stop(this.mirrorNeedsRevert,this.stopDrag.bind(this,e))},t.prototype.stopDrag=function(e){this.isDragging=!1,this.emitter.trigger("dragend",e)},t.prototype.setIgnoreMove=function(e){this.pointer.shouldIgnoreMove=e},t.prototype.setMirrorIsVisible=function(e){this.mirror.setIsVisible(e)},t.prototype.setMirrorNeedsRevert=function(e){this.mirrorNeedsRevert=e},t.prototype.setAutoScrollEnabled=function(e){this.autoScroller.isEnabled=e},t}(oa),As=function(){function e(e){this.origRect=go(e),this.scrollCaches=mo(e).map((function(e){return new Is(e,!0)}))}return e.prototype.destroy=function(){for(var e=0,t=this.scrollCaches;e=0&&c=0&&do.layer)&&(v.componentId=i,v.context=a.context,v.rect.left+=l,v.rect.right+=l,v.rect.top+=u,v.rect.bottom+=u,o=v)}}}return o},e}();function Us(e,t){return!e&&!t||Boolean(e)===Boolean(t)&&fr(e.dateSpan,t.dateSpan)}function Ws(e,t){for(var n,o,i={},a=0,s=t.pluginHooks.datePointTransforms;ar.start)return{endDelta:s};return null}(a,e,r.subjectEl.classList.contains("fc-event-resizer-start"),s.range)));l&&(u=Sr(i,o.getCurrentData().eventUiBases,l,o),d.mutatedEvents=u,Ia(d,e.dateProfile,o)||(c=!0,l=null,u=null,d.mutatedEvents=null)),u?o.dispatch({type:"SET_EVENT_RESIZE",state:d}):o.dispatch({type:"UNSET_EVENT_RESIZE"}),c?Ue():We(),t||(l&&Us(a,e)&&(l=null),n.validMutation=l,n.mutatedRelevantEvents=u)},n.handleDragEnd=function(e){var t=n.component.context,o=n.eventRange.def,i=n.eventRange.instance,a=new xr(t,o,i),s=n.relevantEvents,l=n.mutatedRelevantEvents;if(t.emitter.trigger("eventResizeStop",{el:n.draggingSegEl,event:a,jsEvent:e.origEvent,view:t.viewApi}),n.validMutation){var u=new xr(t,l.defs[o.defId],i?l.instances[i.instanceId]:null);t.dispatch({type:"MERGE_EVENTS",eventStore:l});var c={oldEvent:a,event:u,relatedEvents:Ir(l,t,i),revert:function(){t.dispatch({type:"MERGE_EVENTS",eventStore:s})}};t.emitter.trigger("eventResize",r(r({},c),{el:n.draggingSegEl,startDelta:n.validMutation.startDelta||Nt(0),endDelta:n.validMutation.endDelta||Nt(0),jsEvent:e.origEvent,view:t.viewApi})),t.emitter.trigger("eventChange",c)}else t.emitter.trigger("_noEventResize");n.draggingSeg=null,n.relevantEvents=null,n.validMutation=null};var o=t.component,i=n.dragging=new Os(t.el);i.pointer.selector=".fc-event-resizer",i.touchScrollAllowed=!1,i.autoScroller.isEnabled=o.context.options.dragScroll;var a=n.hitDragging=new Ls(n.dragging,na(t));return a.emitter.on("pointerdown",n.handlePointerDown),a.emitter.on("dragstart",n.handleDragStart),a.emitter.on("hitupdate",n.handleHitUpdate),a.emitter.on("dragend",n.handleDragEnd),n}return n(t,e),t.prototype.destroy=function(){this.dragging.destroy()},t.prototype.querySegEl=function(e){return Se(e.subjectEl,".fc-event")},t}(ea);var js=function(){function e(e){var t=this;this.context=e,this.isRecentPointerDateSelect=!1,this.matchesCancel=!1,this.matchesEvent=!1,this.onSelect=function(e){e.jsEvent&&(t.isRecentPointerDateSelect=!0)},this.onDocumentPointerDown=function(e){var n=t.context.options.unselectCancel,r=_e(e.origEvent);t.matchesCancel=!!Se(r,n),t.matchesEvent=!!Se(r,Bs.SELECTOR)},this.onDocumentPointerUp=function(e){var n=t.context,r=t.documentPointer,o=n.getCurrentData();if(!r.wasTouchScroll){if(o.dateSelection&&!t.isRecentPointerDateSelect){var i=n.options.unselectAuto;!i||i&&t.matchesCancel||n.calendarApi.unselect(e)}o.eventSelection&&!t.matchesEvent&&n.dispatch({type:"UNSELECT_EVENT"})}t.isRecentPointerDateSelect=!1};var n=this.documentPointer=new Ts(document);n.shouldIgnoreMove=!0,n.shouldWatchScroll=!1,n.emitter.on("pointerdown",this.onDocumentPointerDown),n.emitter.on("pointerup",this.onDocumentPointerUp),e.emitter.on("select",this.onSelect)}return e.prototype.destroy=function(){this.context.emitter.off("select",this.onSelect),this.documentPointer.destroy()},e}(),Gs={fixedMirrorParent:yn},qs={dateClick:yn,eventDragStart:yn,eventDragStop:yn,eventDrop:yn,eventResizeStart:yn,eventResizeStop:yn,eventResize:yn,drop:yn,eventReceive:yn,eventLeave:yn},Ys=function(){function e(e,t){var n=this;this.receivingContext=null,this.droppableEvent=null,this.suppliedDragMeta=null,this.dragMeta=null,this.handleDragStart=function(e){n.dragMeta=n.buildDragMeta(e.subjectEl)},this.handleHitUpdate=function(e,t,o){var i=n.hitDragging.dragging,a=null,s=null,l=!1,u={affectedEvents:{defs:{},instances:{}},mutatedEvents:{defs:{},instances:{}},isEvent:n.dragMeta.create};e&&(a=e.context,n.canDropElOnCalendar(o.subjectEl,a)&&(s=function(e,t,n){for(var o=r({},t.leftoverProps),i=0,a=n.pluginHooks.externalDefTransforms;i1,S=y.span.start===s;d+=y.levelCoord-c,c=y.levelCoord+y.thickness,E?(d+=y.thickness,S&&v.push({seg:hl(h,y.span.start,y.span.end,n),isVisible:!0,isAbsolute:!0,absoluteTop:y.levelCoord,marginTop:0})):S&&(v.push({seg:hl(h,y.span.start,y.span.end,n),isVisible:!0,isAbsolute:!1,absoluteTop:y.levelCoord,marginTop:d}),d=0)}o.push(u),i.push(v),a.push(d)}return{singleColPlacements:o,multiColPlacements:i,leftoverMargins:a}}(s.toRects(),e,a),h=f.singleColPlacements,v=f.multiColPlacements,g=f.leftoverMargins,m=[],y=[],E=0,S=u;E1,showWeekNumbers:t.showWeekNumbers,todayRange:h,dateProfile:n,cells:i,renderIntro:t.renderRowIntro,businessHourSegs:s[f],eventSelection:t.eventSelection,bgEventSegs:l[f].filter(yl),fgEventSegs:u[f],dateSelectionSegs:c[f],eventDrag:d[f],eventResize:p[f],dayMaxEvents:o,dayMaxEventRows:r,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:t.forPrint})})))))})))},t.prototype.prepareHits=function(){this.rowPositions=new So(this.rootEl,this.rowRefs.collect().map((function(e){return e.getCellEls()[0]})),!1,!0),this.colPositions=new So(this.rootEl,this.rowRefs.currentMap[0].getCellEls(),!0,!1)},t.prototype.queryHit=function(e,t){var n=this.colPositions,o=this.rowPositions,i=n.leftToIndex(e),a=o.topToIndex(t);if(null!=a&&null!=i){var s=this.props.cells[a][i];return{dateProfile:this.props.dateProfile,dateSpan:r({range:this.getCellRange(a,i),allDay:!0},s.extraDateSpan),dayEl:this.getCellEl(a,i),rect:{left:n.lefts[i],right:n.rights[i],top:o.tops[a],bottom:o.bottoms[a]},layer:0}}return null},t.prototype.getCellEl=function(e,t){return this.rowRefs.currentMap[e].getCellEls()[t]},t.prototype.getCellRange=function(e,t){var n=this.props.cells[e][t].date;return{start:n,end:tt(n,1)}},t}(Bo);function yl(e){return e.eventRange.def.allDay}var El=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.forceDayIfListItem=!0,t}return n(t,e),t.prototype.sliceRange=function(e,t){return t.sliceRange(e)},t}(xa),Sl=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.slicer=new El,t.tableRef=ko(),t}return n(t,e),t.prototype.render=function(){var e=this.props,t=this.context;return _o(ml,r({ref:this.tableRef},this.slicer.sliceProps(e,e.dateProfile,e.nextDayThreshold,t,e.dayTableModel),{dateProfile:e.dateProfile,cells:e.dayTableModel.cells,colGroupNode:e.colGroupNode,tableMinWidth:e.tableMinWidth,renderRowIntro:e.renderRowIntro,dayMaxEvents:e.dayMaxEvents,dayMaxEventRows:e.dayMaxEventRows,showWeekNumbers:e.showWeekNumbers,expandRows:e.expandRows,headerAlignElRef:e.headerAlignElRef,clientWidth:e.clientWidth,clientHeight:e.clientHeight,forPrint:e.forPrint}))},t}(Bo),bl=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.buildDayTableModel=qt(Dl),t.headerRef=ko(),t.tableRef=ko(),t}return n(t,e),t.prototype.render=function(){var e=this,t=this.context,n=t.options,r=t.dateProfileGenerator,o=this.props,i=this.buildDayTableModel(o.dateProfile,r),a=n.dayHeaders&&_o(Ra,{ref:this.headerRef,dateProfile:o.dateProfile,dates:i.headerDates,datesRepDistinctDays:1===i.rowCnt}),s=function(t){return _o(Sl,{ref:e.tableRef,dateProfile:o.dateProfile,dayTableModel:i,businessHours:o.businessHours,dateSelection:o.dateSelection,eventStore:o.eventStore,eventUiBases:o.eventUiBases,eventSelection:o.eventSelection,eventDrag:o.eventDrag,eventResize:o.eventResize,nextDayThreshold:n.nextDayThreshold,colGroupNode:t.tableColGroupNode,tableMinWidth:t.tableMinWidth,dayMaxEvents:n.dayMaxEvents,dayMaxEventRows:n.dayMaxEventRows,showWeekNumbers:n.weekNumbers,expandRows:!o.isHeightAuto,headerAlignElRef:e.headerElRef,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:o.forPrint})};return n.dayMinWidth?this.renderHScrollLayout(a,s,i.colCnt,n.dayMinWidth):this.renderSimpleLayout(a,s)},t}(Js);function Dl(e,t){var n=new Ta(e.renderRange,t);return new ka(n,/year|month|week/.test(e.currentRangeUnit))}var Cl=zo({initialView:"dayGridWeek",views:{dayGrid:{component:bl,dateProfileGeneratorClass:function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.buildRenderRange=function(t,n,r){var o,i=this.props.dateEnv,a=e.prototype.buildRenderRange.call(this,t,n,r),s=a.start,l=a.end;(/^(year|month)$/.test(n)&&(s=i.startOfWeek(s),(o=i.startOfWeek(l)).valueOf()!==l.valueOf()&&(l=et(o,1))),this.props.monthMode&&this.props.fixedWeekCount)&&(l=et(l,6-Math.ceil(rt(s,l))));return{start:s,end:l}},t}(ai)},dayGridDay:{type:"dayGrid",duration:{days:1}},dayGridWeek:{type:"dayGrid",duration:{weeks:1}},dayGridMonth:{type:"dayGrid",duration:{months:1},monthMode:!0,fixedWeekCount:!0}}}),wl=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.getKeyInfo=function(){return{allDay:{},timed:{}}},t.prototype.getKeysForDateSpan=function(e){return e.allDay?["allDay"]:["timed"]},t.prototype.getKeysForEventDef=function(e){return e.allDay?Kn(e)?["timed","allDay"]:["allDay"]:["timed"]},t}(to),Rl=ln({hour:"numeric",minute:"2-digit",omitZeroMinute:!0,meridiem:"short"});function _l(e){var t=["fc-timegrid-slot","fc-timegrid-slot-label",e.isLabeled?"fc-scrollgrid-shrink":"fc-timegrid-slot-minor"];return _o(Oo.Consumer,null,(function(n){if(!e.isLabeled)return _o("td",{className:t.join(" "),"data-time":e.isoTimeStr});var r=n.dateEnv,o=n.options,i=n.viewApi,a=null==o.slotLabelFormat?Rl:Array.isArray(o.slotLabelFormat)?ln(o.slotLabelFormat[0]):ln(o.slotLabelFormat),s={level:0,time:e.time,date:r.toDate(e.date),view:i,text:r.format(e.date,a)};return _o(Yo,{hookProps:s,classNames:o.slotLabelClassNames,content:o.slotLabelContent,defaultContent:Tl,didMount:o.slotLabelDidMount,willUnmount:o.slotLabelWillUnmount},(function(n,r,o,i){return _o("td",{ref:n,className:t.concat(r).join(" "),"data-time":e.isoTimeStr},_o("div",{className:"fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame"},_o("div",{className:"fc-timegrid-slot-label-cushion fc-scrollgrid-shrink-cushion",ref:o},i)))}))}))}function Tl(e){return e.text}var kl=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return n(t,e),t.prototype.render=function(){return this.props.slatMetas.map((function(e){return _o("tr",{key:e.key},_o(_l,r({},e)))}))},t}(Uo),xl=ln({week:"short"}),Ml=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.allDaySplitter=new wl,t.headerElRef=ko(),t.rootElRef=ko(),t.scrollerElRef=ko(),t.state={slatCoords:null},t.handleScrollTopRequest=function(e){var n=t.scrollerElRef.current;n&&(n.scrollTop=e)},t.renderHeadAxis=function(e,n){void 0===n&&(n="");var o=t.context.options,i=t.props.dateProfile.renderRange,a=1===ot(i.start,i.end)?so(t.context,i.start,"week"):{};return o.weekNumbers&&"day"===e?_o(fs,{date:i.start,defaultFormat:xl},(function(e,t,o,i){return _o("th",{ref:e,"aria-hidden":!0,className:["fc-timegrid-axis","fc-scrollgrid-shrink"].concat(t).join(" ")},_o("div",{className:"fc-timegrid-axis-frame fc-scrollgrid-shrink-frame fc-timegrid-axis-frame-liquid",style:{height:n}},_o("a",r({ref:o,className:"fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner"},a),i)))})):_o("th",{"aria-hidden":!0,className:"fc-timegrid-axis"},_o("div",{className:"fc-timegrid-axis-frame",style:{height:n}}))},t.renderTableRowAxis=function(e){var n=t.context,r=n.options,o=n.viewApi,i={text:r.allDayText,view:o};return _o(Yo,{hookProps:i,classNames:r.allDayClassNames,content:r.allDayContent,defaultContent:Il,didMount:r.allDayDidMount,willUnmount:r.allDayWillUnmount},(function(t,n,r,o){return _o("td",{ref:t,"aria-hidden":!0,className:["fc-timegrid-axis","fc-scrollgrid-shrink"].concat(n).join(" ")},_o("div",{className:"fc-timegrid-axis-frame fc-scrollgrid-shrink-frame"+(null==e?" fc-timegrid-axis-frame-liquid":""),style:{height:e}},_o("span",{className:"fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner",ref:r},o)))}))},t.handleSlatCoords=function(e){t.setState({slatCoords:e})},t}return n(t,e),t.prototype.renderSimpleLayout=function(e,t,n){var r=this.context,o=this.props,i=[],a=Qa(r.options);return e&&i.push({type:"header",key:"header",isSticky:a,chunk:{elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}}),t&&(i.push({type:"body",key:"all-day",chunk:{content:t}}),i.push({type:"body",key:"all-day-divider",outerContent:_o("tr",{role:"presentation",className:"fc-scrollgrid-section"},_o("td",{className:"fc-timegrid-divider "+r.theme.getClass("tableCellShaded")}))})),i.push({type:"body",key:"body",liquid:!0,expandRows:Boolean(r.options.expandRows),chunk:{scrollerElRef:this.scrollerElRef,content:n}}),_o(ti,{viewSpec:r.viewSpec,elRef:this.rootElRef},(function(e,t){return _o("div",{className:["fc-timegrid"].concat(t).join(" "),ref:e},_o(ts,{liquid:!o.isHeightAuto&&!o.forPrint,collapsibleWidth:o.forPrint,cols:[{width:"shrink"}],sections:i}))}))},t.prototype.renderHScrollLayout=function(e,t,n,r,o,i,a){var s=this,l=this.context.pluginHooks.scrollGridImpl;if(!l)throw new Error("No ScrollGrid implementation");var u=this.context,c=this.props,d=!c.forPrint&&Qa(u.options),p=!c.forPrint&&es(u.options),f=[];e&&f.push({type:"header",key:"header",isSticky:d,syncRowHeights:!0,chunks:[{key:"axis",rowContent:function(e){return _o("tr",{role:"presentation"},s.renderHeadAxis("day",e.rowSyncHeights[0]))}},{key:"cols",elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}]}),t&&(f.push({type:"body",key:"all-day",syncRowHeights:!0,chunks:[{key:"axis",rowContent:function(e){return _o("tr",{role:"presentation"},s.renderTableRowAxis(e.rowSyncHeights[0]))}},{key:"cols",content:t}]}),f.push({key:"all-day-divider",type:"body",outerContent:_o("tr",{role:"presentation",className:"fc-scrollgrid-section"},_o("td",{colSpan:2,className:"fc-timegrid-divider "+u.theme.getClass("tableCellShaded")}))}));var h=u.options.nowIndicator;return f.push({type:"body",key:"body",liquid:!0,expandRows:Boolean(u.options.expandRows),chunks:[{key:"axis",content:function(e){return _o("div",{className:"fc-timegrid-axis-chunk"},_o("table",{"aria-hidden":!0,style:{height:e.expandRows?e.clientHeight:""}},e.tableColGroupNode,_o("tbody",null,_o(kl,{slatMetas:i}))),_o("div",{className:"fc-timegrid-now-indicator-container"},_o(Ca,{unit:h?"minute":"day"},(function(e){var t=h&&a&&a.safeComputeTop(e);return"number"==typeof t?_o(is,{isAxis:!0,date:e},(function(e,n,r,o){return _o("div",{ref:e,className:["fc-timegrid-now-indicator-arrow"].concat(n).join(" "),style:{top:t}},o)})):null}))))}},{key:"cols",scrollerElRef:this.scrollerElRef,content:n}]}),p&&f.push({key:"footer",type:"footer",isSticky:!0,chunks:[{key:"axis",content:Ja},{key:"cols",content:Ja}]}),_o(ti,{viewSpec:u.viewSpec,elRef:this.rootElRef},(function(e,t){return _o("div",{className:["fc-timegrid"].concat(t).join(" "),ref:e},_o(l,{liquid:!c.isHeightAuto&&!c.forPrint,collapsibleWidth:!1,colGroups:[{width:"shrink",cols:[{width:"shrink"}]},{cols:[{span:r,minWidth:o}]}],sections:f}))}))},t.prototype.getAllDayMaxEventProps=function(){var e=this.context.options,t=e.dayMaxEvents,n=e.dayMaxEventRows;return!0!==t&&!0!==n||(t=void 0,n=5),{dayMaxEvents:t,dayMaxEventRows:n}},t}(Bo);function Il(e){return e.text}var Pl=function(){function e(e,t,n){this.positions=e,this.dateProfile=t,this.slotDuration=n}return e.prototype.safeComputeTop=function(e){var t=this.dateProfile;if(Zn(t.currentRange,e)){var n=lt(e),r=e.valueOf()-n.valueOf();if(r>=Ut(t.slotMinTime)&&r0,E=Boolean(l)&&l.span.end-l.span.start=0;t-=1)if(null!==(r=Wt(n=Nt(ru[t]),e))&&r>1)return n;return e}(r),u=[];Ut(a)0?e.renderSegList(s,i):e.renderEmptyMessage()))}))},t.prototype.renderEmptyMessage=function(){var e=this.context,t=e.options,n=e.viewApi,r={text:t.noEventsText,view:n};return _o(Yo,{hookProps:r,classNames:t.noEventsClassNames,content:t.noEventsContent,defaultContent:hu,didMount:t.noEventsDidMount,willUnmount:t.noEventsWillUnmount},(function(e,t,n,r){return _o("div",{className:["fc-list-empty"].concat(t).join(" "),ref:e},_o("div",{className:"fc-list-empty-cushion",ref:n},r))}))},t.prototype.renderSegList=function(e,t){var n=this.context,o=n.theme,i=n.options,a=this.state,s=a.timeHeaderId,l=a.eventHeaderId,u=a.dateHeaderIdRoot,c=function(e){var t,n,r=[];for(t=0;t=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + \ No newline at end of file diff --git a/web/templates/500.html b/web/templates/500.html new file mode 100644 index 0000000..51234de --- /dev/null +++ b/web/templates/500.html @@ -0,0 +1,43 @@ + +{% import 'macro/svg.html' as SVG %} + + + + + + + + + + + + 500 - NAStool + + + + + +
+
+
+
500
+

出错啦!

+

+ 系统出错了,请检查运行日志看看吧... +

+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/web/templates/discovery/mediainfo.html b/web/templates/discovery/mediainfo.html new file mode 100644 index 0000000..cb2e4a1 --- /dev/null +++ b/web/templates/discovery/mediainfo.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/templates/discovery/person.html b/web/templates/discovery/person.html new file mode 100644 index 0000000..022fc99 --- /dev/null +++ b/web/templates/discovery/person.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/templates/discovery/ranking.html b/web/templates/discovery/ranking.html new file mode 100644 index 0000000..d838170 --- /dev/null +++ b/web/templates/discovery/ranking.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/templates/discovery/recommend.html b/web/templates/discovery/recommend.html new file mode 100644 index 0000000..a9eae03 --- /dev/null +++ b/web/templates/discovery/recommend.html @@ -0,0 +1,184 @@ +{% import 'macro/oops.html' as OOPS %} +{% import 'macro/form.html' as FORM %} +
+ + +
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/download/downloading.html b/web/templates/download/downloading.html new file mode 100644 index 0000000..8151b59 --- /dev/null +++ b/web/templates/download/downloading.html @@ -0,0 +1,206 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +
+ + +
+{% if DownloadCount > 0 %} +
+
+
+ {% for Torrent in Torrents %} +
+
+
+ {% if Torrent.image %} +
+ +
+ {% endif %} +
+

+ {{ Torrent.title }} +

+
+ {{ Torrent.speed }} +
+ {% if not Torrent.noprogress %} +
+
+
+ {{ Torrent.progress }}% +
+
+
+
+ +
+
+
+
+
+ {% endif %} +
+
+ +
+ +
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有下载任务', '当前下载器中没有正在下载的任务。') }} +{% endif %} + \ No newline at end of file diff --git a/web/templates/download/torrent_remove.html b/web/templates/download/torrent_remove.html new file mode 100644 index 0000000..b8eb182 --- /dev/null +++ b/web/templates/download/torrent_remove.html @@ -0,0 +1,679 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +
+ + +
+ +{% if Count > 0 %} +
+
+
+ {% for Id, Attr in TorrentRemoveTasks.items() %} +
+ +
+
+
+
下载器
+
+ {{ DownloaderConfig[Attr.downloader].name}} +
+
+
+
管理限制
+
+ {% if Attr.onlynastool %} + 只管理NAStool添加 + {% endif %} + {% if Attr.samedata %} + 处理辅种 + {% endif %} +
+
+
+
刷新间隔
+
+ {{ Attr.interval }} 分钟 +
+
+
+
动作
+
+ {% if Attr.action == 1 %} + 暂停任务 + {% elif Attr.action == 2 %} + 删除任务 + {% elif Attr.action == 3 %} + 删除任务及文件 + {% endif %} +
+
+
+
分享率
+
+ {% if Attr.config.ratio %} + {{ Attr.config.ratio }} + + {% endif %} +
+
+
+
做种时间
+
+ {% if Attr.config.seeding_time %} + {{ Attr.config.seeding_time }} 小时 + + {% endif %} +
+
+
+
平均做种速度
+
+ {% if Attr.config.upload_avs %} + {{ Attr.config.upload_avs }} KB/s - + {% endif %} +
+
+
+
大小
+
+ {% if Attr.config.size %} + {{ Attr.config.size[0] }}-{{ Attr.config.size[-1] }} GB + {% endif %} +
+
+
+
保存路径关键词
+
+ {% if Attr.config.savepath_key %} + {{ Attr.config.savepath_key }} + {% endif %} +
+
+
+
tracker关键词
+
+ {% if Attr.config.tracker_key %} + {{ Attr.config.tracker_key }} + {% endif %} +
+
+ {% if Attr.downloader == "Qb" %} +
+
分类
+
+ {% if Attr.config.qb_category %} + {% for Category in Attr.config.qb_category %} + {{ Category }} + {% endfor %} + {% endif %} +
+
+
+
种子状态
+
+ {% if Attr.config.qb_state %} + {% for State in Attr.config.qb_state %} + {{ State }} + {% endfor %} + {% endif %} +
+
+ {% elif Attr.downloader == "Tr" %} +
+
错误信息关键词
+
+ {% if Attr.config.tr_error_key %} + {{ Attr.config.tr_error_key }} + {% endif %} +
+
+
+
种子状态
+
+ {% if Attr.config.tr_state %} + {% for State in Attr.config.tr_state %} + {{ State }} + {% endfor %} + {% endif %} +
+
+ {% endif %} +
+
状态
+
+ {% if Attr.enabled %} + 正在运行 + {% else %} + 已停用 + {% endif %} +
+
+
+
标签
+
+ {% if Attr.config.tags %} + {% for Tag in Attr.config.tags %} + {{ Tag }} + {% endfor %} + {% endif %} +
+
+
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有任务', '当前没有正在运行的自动删种任务。') }} +{% endif %} + + + \ No newline at end of file diff --git a/web/templates/download/userdownloader.html b/web/templates/download/userdownloader.html new file mode 100644 index 0000000..7d20e49 --- /dev/null +++ b/web/templates/download/userdownloader.html @@ -0,0 +1,267 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+
+
+
+
+
+ 共 {{ Count }} 条记录 +
+
+
+
+ + + + + + + + + + + {% if Downloaders %} + {% for Downloader in Downloaders %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
名称类型地址
{{ Downloader.name or '' }}{{ Downloader.type or '' }}{{ Downloader.host }}:{{ Downloader.port }} + +
没有数据
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..4aaced9 --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,349 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +
+ + +
+{% if ServerSucess %} +
+
+
+
+
+
+
+
电影
+
+
+
{{ MediaCount.MovieCount }}
+
+
+
+
+
+
+
+
+
+
电视剧/动漫
+
+
+
{{ MediaCount.SeriesCount }}
+
+ + {{ MediaCount.EpisodeCount }} + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
音乐
+
+
+
{{ MediaCount.SongCount }}
+
+
+
+
+
+
+
+
+ +
+
+
+
+
活跃用户
+
+
+
{{ UserCount }}
+
+
+
+
+
+
+
+
+
+

存储空间共 {{ TotalSpace }}

+
+
+
+
+
+ + 已使用 + {{ UsedSapce }} +
+
+ + 空闲 + {{ FreeSpace }} +
+
+
+
+
+
+
+
+
+ {% for Activity in Activitys %} +
+
+
+ + {% if Activity.type == "LG" %} + {{ SVG.user() }} + {% else %} + {{ SVG.player_play() }} + {% endif %} + +
+
+
+ {{ Activity.event }} +
+
{{ Activity.date }}
+
+
+
+
+
+
+ {% endfor %} +
+
+
+
+
+
+
+
+
+{% else %} +{{ OOPS.systemerror('媒体服务器连接失败!', '当前无法连接媒体服务器获取数据,请确认Emby/Jellyfin/Plex配置是否正确。') }} +{% endif %} + + + \ No newline at end of file diff --git a/web/templates/login.html b/web/templates/login.html new file mode 100644 index 0000000..4fbe726 --- /dev/null +++ b/web/templates/login.html @@ -0,0 +1,58 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/head.html' as HEAD %} + + + + {{ HEAD.meta_link() }} + 登录 - NAStool + + + + + +
+
+
+ +
+
+ +
+ +
+ + {{ SVG.user() }} + + +
+ +
+ + {{ SVG.keyboard() }} + + +
+
{{ err_msg }}
+
+ +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/web/templates/macro/form.html b/web/templates/macro/form.html new file mode 100644 index 0000000..606595a --- /dev/null +++ b/web/templates/macro/form.html @@ -0,0 +1,112 @@ + +{% macro gen_form_config_elements(Type, Config, Fields) %} + {% for FieldId, FieldAttr in Fields.items() %} + {% if loop.index%2 == 1 %} +
+ {% endif %} +
+
+ {% if FieldAttr.type == "switch" %} + + {% else %} + + {% if FieldAttr.type == "select" %} + + {% else %} + + {% endif %} + {% endif %} +
+
+ {% if loop.last or loop.index%2 == 0 %} +
+ {% endif %} + {% endfor %} +{% endmacro %} + + +{% macro gen_form_empty_elements(Fields) %} + {% for FieldId, FieldAttr in Fields.items() %} + {% if loop.index%2 == 1 %} +
+ {% endif %} +
+
+ {% if FieldAttr.type == "switch" %} + + {% else %} + + {% if FieldAttr.type == "select" %} + + {% else %} + + {% endif %} + {% endif %} +
+
+ {% if loop.last or loop.index%2 == 0 %} +
+ {% endif %} + {% endfor %} +{% endmacro %} + + +{% macro gen_recommend_filter_dropdown(Fields, Params) %} + {% if Fields %} + {% for FieldId, FieldAttr in Fields.items() %} + {% if FieldAttr.type == "dropdown" %} + + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} diff --git a/web/templates/macro/head.html b/web/templates/macro/head.html new file mode 100644 index 0000000..17839c8 --- /dev/null +++ b/web/templates/macro/head.html @@ -0,0 +1,64 @@ + +{% macro meta_link() %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endmacro %} + diff --git a/web/templates/macro/oops.html b/web/templates/macro/oops.html new file mode 100644 index 0000000..ffe739d --- /dev/null +++ b/web/templates/macro/oops.html @@ -0,0 +1,62 @@ + +{% macro nodatafound(title, text) %} +
+
+
+
+
+

{{ title }}

+

+ {{ text }} +

+
+
+
+{% endmacro %} + + +{% macro empty(title, text) %} +
+
+
+
+
+

{{ title }}

+

+ {{ text }} +

+
+
+
+{% endmacro %} + + +{% macro systemerror(title, text) %} +
+
+
+
+
+

{{ title }}

+

+ {{ text }} +

+
+
+
+{% endmacro %} + + +{% macro loading() %} +
+
+
+
+
+

+ 页面正在加载,请稍候... +

+
+
+
+{% endmacro %} diff --git a/web/templates/macro/svg.html b/web/templates/macro/svg.html new file mode 100644 index 0000000..18413d3 --- /dev/null +++ b/web/templates/macro/svg.html @@ -0,0 +1,1081 @@ + + +{% macro plus(class) %} + + + + + + +{% endmacro %} + + + +{% macro history(class) %} + + + + + + +{% endmacro %} + + + +{% macro arrow_left(class) %} + + + + + + + +{% endmacro %} + + + +{% macro refresh(class) %} + + + + + + +{% endmacro %} + + + +{% macro refresh_dot(class) %} + + + + + + + +{% endmacro %} + + + +{% macro user(class) %} + + + + + + +{% endmacro %} + + + +{% macro player_play(class) %} + + + + + +{% endmacro %} + + + +{% macro dots_vertical(class) %} + + + + + + + +{% endmacro %} + + + +{% macro bolt(class) %} + + + + +{% endmacro %} + + + +{% macro menu_2(class) %} + + + + + + + +{% endmacro %} + + + +{% macro eye(class) %} + + + + + + +{% endmacro %} + + + +{% macro edit(class) %} + + + + + + + +{% endmacro %} + + + +{% macro x(class) %} + + + + + + +{% endmacro %} + + + +{% macro arrow_back_up(class) %} + + + + + +{% endmacro %} + + + +{% macro keyboard(class) %} + + + + + + + + + + + + +{% endmacro %} + + + +{% macro rss(class) %} + + + + + + + +{% endmacro %} + + + +{% macro search(class) %} + + + + + + +{% endmacro %} + + + +{% macro adjustments(class) %} + + + + + + + + + + + + +{% endmacro %} + + + +{% macro home(class) %} + + + + + + + +{% endmacro %} + + + +{% macro trash(class) %} + + + + + + + + + +{% endmacro %} + + + +{% macro star(class) %} + + + + +{% endmacro %} + + + +{% macro server_2(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro movie(class) %} + + + + + + + + + + + + +{% endmacro %} + + + +{% macro device_tv(class) %} + + + + + + +{% endmacro %} + + + +{% macro chevron_left(class) %} + + + + + +{% endmacro %} + + + +{% macro chevron_right(class) %} + + + + + +{% endmacro %} + + + +{% macro folders(class) %} + + + + + + +{% endmacro %} + + + +{% macro transform(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro link(class) %} + + + + + + +{% endmacro %} + + + +{% macro file_info(class) %} + + + + + + + + +{% endmacro %} + + + +{% macro eraser(class) %} + + + + + + +{% endmacro %} + + + +{% macro cpu(class) %} + + + + + + + + + + + + + + +{% endmacro %} + + + +{% macro download(class) %} + + + + + + + +{% endmacro %} + + + +{% macro circle_check(class) %} + + + + + +{% endmacro %} + + + +{% macro circle_x(class) %} + + + + + + +{% endmacro %} + + + +{% macro text_recognition(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro check(class) %} + + + + + +{% endmacro %} + + + +{% macro video(class) %} + + + + + + +{% endmacro %} + + + +{% macro info_circle(class) %} + + + + + + + +{% endmacro %} + + + +{% macro dots(class) %} + + + + + + + +{% endmacro %} + + + +{% macro slideshow(class) %} + + + + + + + + + + + +{% endmacro %} + + + +{% macro tex(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro log_select(class) %} + + +{% endmacro %} + + + +{% macro info_square_rounded(class) %} + + + + + + + +{% endmacro %} + + + +{% macro photo(class) %} + + + + + + + + +{% endmacro %} + + + +{% macro player_stop(class) %} + + + + + +{% endmacro %} + + + +{% macro transfer_in(class) %} + + + + + + + +{% endmacro %} + + + +{% macro transfer_out(class) %} + + + + + + + +{% endmacro %} + + + +{% macro settings(class) %} + + + + + + +{% endmacro %} + + + +{% macro circle_minus(class) %} + + + + + + +{% endmacro %} + + + +{% macro folder_plus(class) %} + + + + + + + +{% endmacro %} + + + +{% macro reload(class) %} + + + + + + +{% endmacro %} + + + +{% macro share(class) %} + + + + + + + + + +{% endmacro %} + + + +{% macro apps(class) %} + + + + + + + + + +{% endmacro %} + + + +{% macro arrow_big_down(class) %} + + + + + +{% endmacro %} + + + +{% macro activity_heartbeat(class) %} + + + + + +{% endmacro %} + + + +{% macro world_upload(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro world_download(class) %} + + + + + + + + + + +{% endmacro %} + + + +{% macro arrow_big_up_lines(class) %} + + + + + + + +{% endmacro %} + + + +{% macro cloud_upload(class) %} + + + + + + + +{% endmacro %} + + + +{% macro message(class) %} + + + + + + + +{% endmacro %} + + + +{% macro arrow_narrow_up(class) %} + + + + + + + +{% endmacro %} + + + +{% macro arrow_narrow_down(class) %} + + + + + + + +{% endmacro %} + + + +{% macro activity(class) %} + + + + + +{% endmacro %} + + + +{% macro checkbox(class) %} + + + + + + +{% endmacro %} + + + +{% macro layout_2(class) %} + + + + + + + + +{% endmacro %} + + + +{% macro brand_github(class) %} + + + + + +{% endmacro %} + + + +{% macro moon(class) %} + + + + + +{% endmacro %} + + + +{% macro sun(class) %} + + + + + + +{% endmacro %} + + + +{% macro bell(class) %} + + + + + + +{% endmacro %} + + + +{% macro alert_triangle(class) %} + + + + + +{% endmacro %} + + + +{% macro heart(class) %} + + + + + +{% endmacro %} + + + +{% macro send(class) %} + + + + + + +{% endmacro %} + + + +{% macro circle_letter_d(class) %} + + + + + + +{% endmacro %} + + + +{% macro square_letter_t(class) %} + + + + + + + +{% endmacro %} + + + +{% macro code_dots(class) %} + + + + + + + + + +{% endmacro %} + + + +{% macro forbid(class) %} + + + + + + +{% endmacro %} + + + +{% macro filter(class) %} + + + + + +{% endmacro %} \ No newline at end of file diff --git a/web/templates/navigation.html b/web/templates/navigation.html new file mode 100644 index 0000000..a76c9a1 --- /dev/null +++ b/web/templates/navigation.html @@ -0,0 +1,2468 @@ + +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +{% import 'macro/head.html' as HEAD %} + + + {{ HEAD.meta_link() }} + NAStool + + + + + + + + + + + + + +
+ + + + + + +
+ +
+
+
+

消息中心

+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/templates/rename/history.html b/web/templates/rename/history.html new file mode 100644 index 0000000..b330bc7 --- /dev/null +++ b/web/templates/rename/history.html @@ -0,0 +1,282 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+
+
+
+
+
+ 共 {{ TotalCount }} 条记录 +
+
+ 搜索: +
+ +
+
+
+
+
+ + + + {% if TotalCount > 0 %} + + {% endif %} + + + + + + + + {% if TotalCount > 0 %} + {% for History in Historys %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + 媒体信息文件信息时间
+ + +
+ + {% if History.TYPE == "电影" %} + {{ SVG.movie() }} + {% else %} + {{ SVG.device_tv() }} + {% endif %} + +
+
+ {% if History.TMDBID %} + + {{ History.TITLE }} ({{ History.YEAR }}) + + {% if History.SEASON_EPISODE %} +
+ {{ History.SEASON_EPISODE }} + {% endif %} + {% else %} + {{ History.TITLE }} ({{ History.YEAR }}) + {% if History.SEASON_EPISODE %} + {{ History.SEASON_EPISODE }} + {% endif %} + {% endif %} +
+ {% if History.CATEGORY %} +
类别:{{ History.CATEGORY }}
+ {% endif %} +
+
+
+ +
+ {% if History.DEST_PATH or History.DEST_FILENAME %} + => + {% endif %} + {% if History.DEST_PATH %} + + {{ History.DEST_FILENAME or '' }} + + {% else %} + {{ History.DEST_FILENAME or '' }} + {% endif %} +
+
+
+ {{ History.DATE }} +
来自:{{ History.SOURCE or '' }}
+
转移方式:{{ History.SYNC_MODE }}
+
+
+ +
没有数据
+
+ {% if TotalCount > 0 %} + + {% endif %} +
+
+
+
+
+ \ No newline at end of file diff --git a/web/templates/rename/mediafile.html b/web/templates/rename/mediafile.html new file mode 100644 index 0000000..269c3df --- /dev/null +++ b/web/templates/rename/mediafile.html @@ -0,0 +1,430 @@ +{% import 'macro/svg.html' as SVG %} +
+
+
+ +
+
+
+

目录

+
+ + {{ SVG.folders() }} + + +
+ + {{ SVG.arrow_back_up() }} + 上级目录 + +
+
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/web/templates/rename/tmdbcache.html b/web/templates/rename/tmdbcache.html new file mode 100644 index 0000000..8a76db2 --- /dev/null +++ b/web/templates/rename/tmdbcache.html @@ -0,0 +1,266 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+
+
+
+
+
+ 共 {{ TotalCount }} 条记录 +
+
+ 搜索: +
+ +
+
+
+
+
+ + + + + + + + + + + + {% if TotalCount > 0 %} + {% for TmdbCache in TmdbCaches %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
索引标题TMDB
+
+ + {% if TmdbCache[1].media_type == "电影" %} + {{ SVG.movie() }} + {% else %} + {{ SVG.device_tv() }} + {% endif %} + +
+
{{ TmdbCache[2] }}
+
+
+
+
{{ TmdbCache[1].title }}
+
+
{{ TmdbCache[1].id }} + {% if TmdbCache[1].media_type == "电影" %} + + {{ SVG.link() }} + + {% else %} + + {{ SVG.link() }} + + {% endif %} +
+
+ {% if TmdbCache[1].poster_path %} + + {% endif %} + + +
没有数据
+
+ {% if TotalCount > 0 %} + + {% endif %} +
+
+
+
+
+ + + \ No newline at end of file diff --git a/web/templates/rename/unidentification.html b/web/templates/rename/unidentification.html new file mode 100644 index 0000000..eb3061c --- /dev/null +++ b/web/templates/rename/unidentification.html @@ -0,0 +1,162 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+
+
+
+
+
+ 共 {{ TotalCount }} 条记录 +
+
+ +
+
+ + + + {% if TotalCount > 0 %} + + {% endif %} + + + + + + + {% if TotalCount > 0 %} + {% for Path in Items %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + 文件名转移方式
+ + + {{ Path.name }} + {% if Path.to %} +
+ => {{ Path.to }} +
+ {% endif %} +
+
+ {{ Path.sync_mode }} +
+
+ +
没有数据
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/web/templates/rss/movie_rss.html b/web/templates/rss/movie_rss.html new file mode 100644 index 0000000..b1cf1e4 --- /dev/null +++ b/web/templates/rss/movie_rss.html @@ -0,0 +1,126 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + +{% if Count > 0 %} + + +{% else %} +{{ OOPS.nodatafound('没有订阅', '当前没有正在订阅的电影。') }} +{% endif %} diff --git a/web/templates/rss/rss_calendar.html b/web/templates/rss/rss_calendar.html new file mode 100644 index 0000000..ce98e53 --- /dev/null +++ b/web/templates/rss/rss_calendar.html @@ -0,0 +1,113 @@ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/web/templates/rss/rss_history.html b/web/templates/rss/rss_history.html new file mode 100644 index 0000000..ae3763f --- /dev/null +++ b/web/templates/rss/rss_history.html @@ -0,0 +1,127 @@ +{% import 'macro/svg.html' as SVG %} + + + +
+
+
+
+
+
+
+
+ 共 {{ Count }} 条记录 +
+
+
+
+ + + + + + + + + + + + {% if Count > 0 %} + {% for Item in Items %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
标题简介完成时间
+ + +
{{ Item.NAME }} ({{ Item.YEAR }}) {{ Item.SEASON or '' }}
+ {{ Item.TMDBID }} + {% if Item.TOTAL %} +
共 {{ Item.TOTAL - (Item.START or 0) }} 集 +
+ {% endif %} +
+ {{ Item.DESC }} + + {{ Item.FINISH_TIME }} + + +
没有完成的订阅
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/web/templates/rss/rss_parser.html b/web/templates/rss/rss_parser.html new file mode 100644 index 0000000..f3e2c87 --- /dev/null +++ b/web/templates/rss/rss_parser.html @@ -0,0 +1,221 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+
+
+
+
+
+ 共 {{ Count }} 条记录 +
+
+
+
+ + + + + + + + + + + {% if RssParsers %} + {% for RssParser in RssParsers %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
名称类型附加参数
{{ RssParser.name or '' }}{{ RssParser.type or '' }}{{ RssParser.params or '' }} + +
没有数据
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/rss/tv_rss.html b/web/templates/rss/tv_rss.html new file mode 100644 index 0000000..0a4d6c5 --- /dev/null +++ b/web/templates/rss/tv_rss.html @@ -0,0 +1,137 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + +{% if Count > 0 %} + + +{% else %} +{{ OOPS.nodatafound('没有订阅', '当前没有正在订阅的电视剧。') }} +{% endif %} diff --git a/web/templates/rss/user_rss.html b/web/templates/rss/user_rss.html new file mode 100644 index 0000000..05a2ed6 --- /dev/null +++ b/web/templates/rss/user_rss.html @@ -0,0 +1,960 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + +{% if Count > 0 %} +
+
+
+ {% for Task in Tasks %} +
+
+ +
+ {% if Task.state == 'Y' %} + + {% else %} + + {% endif %} +
+
+ +

{{ Task.name }}

+
+ + {% if Task.uses == 'D' and Task.recognization == "Y" %} + TMDB + + {% else %} + + {% endif %} + + +
+
+
+
+
地址
+
+
+ {{ Task.address|split('?', 0) }} +
+
+
+
+
解析器
+
+ {{ Task.parser_name }} +
+
+
+
刷新间隔
+
+ {{ Task.interval }} 分钟 +
+
+
+
动作
+
+ {{ Task.uses_text }} +
+
+
+
包含
+
+ {% if Task.include %} + {{ Task.include }} + {% endif %} +
+
+
+
排除
+
+ {% if Task.exclude %} + {{ Task.exclude }} + {% endif %} +
+
+
+
过滤规则
+
+ {% if Task.filter_name %} + {{ Task.filter_name }} + {% endif %} +
+
+
+
保存路径
+
{{ Task.save_path or '自动' }}
+
+
+
状态
+
+ {% if Task.state == 'Y' %} + 正在运行 + {% else %} + 已停用 + {% endif %} +
+
+
+
已处理数量
+
+ {% if Task.uses == 'D' %} + + {{ Task.counter or 0 }} + + {% else %} + {{ Task.counter or 0 }} + {% endif %} +
+
+
+
最后更新时间
+
+ {{ Task.update_time or '' }} +
+
+ {% if Task.uses == 'D' %} +
+
下载设置
+
+ {% if Task.download_setting|string in DownloadSettings %} + {{ DownloadSettings[Task.download_setting|string] }} + {% else %} + 默认 + {% endif %} +
+
+ {% endif %} +
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有订阅任务', '当前没有自定义订阅任何内容。') }} +{% endif %} + + + + \ No newline at end of file diff --git a/web/templates/search.html b/web/templates/search.html new file mode 100644 index 0000000..93097e3 --- /dev/null +++ b/web/templates/search.html @@ -0,0 +1,489 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +
+ + +
+ +{% if Count > 0 and Results|length > 0 %} +
+
+
+ {% for Title, Item in Results.items() %} +
+ + + {% if Item.exist %} +
+ {{ SVG.check() }} +
+ {% endif %} +
+
+
+ + {% if Item.poster %} + + {% endif %} + + {% if Item.filter.season %} +
+
+ {% for filter_season in Item.filter.season %} + + {% endfor %} +
+ {% endif %} +
站点
+
+ {% for filter_site in Item.filter.site %} + + {% endfor %} +
+
促销
+
+ {% for filter_free in Item.filter.free %} + + {% endfor %} +
+ {% if Item.filter.video %} +
视频编码
+
+ {% for filter_video in Item.filter.video %} + + {% endfor %} +
+ {% endif %} +
+ +
+
+
+ +
+
+

+ {{ Title }} +

+
+
+ +
+
+ {% if Item.tmdbid and Item.tmdbid!= '0' %} +
+
+ {{ SVG.video() }} + {{ Item.type }} +
+
+ {{ SVG.star() }} + {{ Item.vote or '暂无评分' }} +
+
+ {{ SVG.info_circle() }} + {{ Item.tmdbid }} +
+
+ {% endif %} +
+
+ + {% if Item.overview %} +
+ {{ Item.overview}} +
+ {% endif %} + + {% for SE_key, SE_dict in Item.torrent_dict %} + {% if SE_key != 'MOV' %} + +
+

+ {{ SE_key }} +

+
+
+ {% endif %} +
+
+ {% for group_key, group in SE_dict.items() %} +
+

+ +

+
+
+
+ + {% for unique_key, unique in group.group_torrents.items() %} + {% for torrent in unique.torrent_list %} +
+
+ +
+ {{ torrent.torrent_name }} +
+ {{ torrent.description or '' }} +
+
+ {{ torrent.site }} + {% if torrent.video_encode %} + {{ torrent.video_encode }} + {% endif %} + {% if torrent.reseffect %} + {{ torrent.reseffect }} + {% endif %} + {% if torrent.size %} + {{ torrent.size }} + {% endif %} + {% if torrent.releasegroup %} + {{ torrent.releasegroup }} + {% endif %} + {% if torrent.uploadvalue != 1.0 %} + {{ (torrent.uploadvalue * 100) | int }}%UL + {% endif %} + {% if torrent.downloadvalue != 1.0 %} + {% if torrent.downloadvalue == 0.0 %} + FREE + {% else %} + {{ (torrent.downloadvalue * 100) | int }}%DL + {% endif %} + {% endif %} + {% if torrent.seeders %} + {{ torrent.seeders }}{{ UPCHAR }} + {% endif %} +
+
+ +
+ +
+
+
+ {% endfor %} + {% endfor %} +
+
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.empty('没有搜索结果', '输入想看的电影、电视剧名称,点击搜索试试看吧。') }} +{% endif %} + +{% for Title, Item in Results.items() %} + +{% endfor %} + + diff --git a/web/templates/service.html b/web/templates/service.html new file mode 100644 index 0000000..4c2374a --- /dev/null +++ b/web/templates/service.html @@ -0,0 +1,403 @@ +
+ + +
+ +{% if Count > 0 %} +
+
+
+ {% for Scheduler in SchedulerTasks %} + + {% endfor %} +
+
+
+{% else %} +
+
+
+
+
+

没有服务

+

+ 没有开启任何后台服务。 +

+
+
+
+{% endif %} + + + + + \ No newline at end of file diff --git a/web/templates/setting/basic.html b/web/templates/setting/basic.html new file mode 100644 index 0000000..842e4fc --- /dev/null +++ b/web/templates/setting/basic.html @@ -0,0 +1,1132 @@ +{% import 'macro/svg.html' as SVG %} +
+ + +
+ +
+
+
+
+
+
+

系统

+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+

媒体

+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

服务

+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

安全

+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+

实验室

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + diff --git a/web/templates/setting/customwords.html b/web/templates/setting/customwords.html new file mode 100644 index 0000000..22722ae --- /dev/null +++ b/web/templates/setting/customwords.html @@ -0,0 +1,915 @@ +{% import 'macro/svg.html' as SVG %} + + + +
+
+
+ {% for Group in Groups %} +
+
+ +
8 %}style="display: none;"{% + endif %}> + + + + {% if Group.words %} + + {% endif %} + + + + + + + + + + + + + {% if Group.words %} + {% for Word in Group.words %} + + + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + 被替换词替换词偏移集数前定位词后定位词状态备注
+ + {{ Word.replaced }}{{ Word.replace }} + {% if Word.offset %} + {{ Word.offset + }} + {% endif %} + {{ Word.front }}{{ Word.back }} + {% if Word.regex == 1 %} + RegEx + {% endif %} + + {% if Word.season != -2 %} + {% if Word.season == -1 %} + 全部季 + {% else %} + 第{{ + Word.season }}季 + {% endif %} + {% endif %} + {% if Word.help %} + ? + {% endif %} + + +
未配置
+
+
+
+ {% endfor %} +
+
+
+ + + + + + + \ No newline at end of file diff --git a/web/templates/setting/directorysync.html b/web/templates/setting/directorysync.html new file mode 100644 index 0000000..4f261ad --- /dev/null +++ b/web/templates/setting/directorysync.html @@ -0,0 +1,272 @@ +{% import 'macro/svg.html' as SVG %} +
+ + +
+ +
+
+
+
+
+
+
+
+ 共 {{ SyncCount }} 条记录 +
+
+
+
+ + + + + + + + + + + + + {% if SyncPaths %} + {% for SyncPath in SyncPaths %} + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
源目录目的目录同步方式识别重命名状态
{{ SyncPath.from or '' }} + {{ SyncPath.to or '' }} + {% if SyncPath.unknown and SyncPath.rename %} +
+ 未识别:{{ SyncPath.unknown }} +
+ {% endif %} +
+ {{ SyncPath.syncmod_name }} + + + + + + +
未配置
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/setting/douban.html b/web/templates/setting/douban.html new file mode 100644 index 0000000..e9c8242 --- /dev/null +++ b/web/templates/setting/douban.html @@ -0,0 +1,218 @@ +{% import 'macro/svg.html' as SVG %} +
+ + +
+ +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/setting/download_setting.html b/web/templates/setting/download_setting.html new file mode 100644 index 0000000..a63bc89 --- /dev/null +++ b/web/templates/setting/download_setting.html @@ -0,0 +1,409 @@ +{% import 'macro/svg.html' as SVG %} + + +
+
+
+ {% for Id, Attr in DownloadSetting.items() %} +
+
+ +

{{ Attr.name }}

+
+ {% if DefaultDownloadSetting == Id %} + + {% else %} + + {% endif %} + + + +
+
+
+
+
下载器
+
+ {{ Attr.downloader or '默认' }} +
+
+
+
分类
+
+ {% if Attr.category %} + {{ Attr.category }} + {% endif %} +
+
+
+
标签
+
+ {% if Attr.tags %} + {% for Tag in Attr.tags.split(";") %} + {{ Tag }} + {% endfor %} + {% endif %} +
+
+
+
布局
+
+ {% if Attr.content_layout == 0 %} + 全局 + {% endif %} + {% if Attr.content_layout == 1 %} + 原始 + {% endif %} + {% if Attr.content_layout == 2 %} + 创建子文件夹 + {% endif %} + {% if Attr.content_layout == 3 %} + 不建子文件夹 + {% endif %} +
+
+
+
动作
+
+ {% if Attr.is_paused %} + 添加后暂停 + {% else %} + 添加后开始 + {% endif %} +
+
+
+
上传速度限制
+
+ {% if Attr.upload_limit %} + {{ Attr.upload_limit|string + ' KB/s' or '' }} + {% endif %} +
+
+
+
下载速度限制
+
+ {% if Attr.download_limit %} + {{ Attr.download_limit|string + ' KB/s' or ''}} + {% endif %} +
+
+
+
分享率限制
+
+ {{ Attr.ratio_limit or '' }} +
+
+
+
做种时间限制
+
+ {% if Attr.seeding_time_limit %} + {{ Attr.seeding_time_limit|string + ' 分钟' or '' }} + {% endif %} +
+
+
+
+
+ {% endfor %} +
+
+
+ + \ No newline at end of file diff --git a/web/templates/setting/downloader.html b/web/templates/setting/downloader.html new file mode 100644 index 0000000..97fd595 --- /dev/null +++ b/web/templates/setting/downloader.html @@ -0,0 +1,464 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/form.html' as FORM %} + + +
+
+
+ {% for Type, Downloader in DownloaderConf.items() %} + +
+ + +
+
+
{{ Downloader.name }}
+
{% if Config.pt.pt_client == Type %} + 默认使用{% endif %}
+
+
+ {% endfor %} +
+
+
+{% for Type, Downloader in DownloaderConf.items() %} + +{% endfor %} + + + + \ No newline at end of file diff --git a/web/templates/setting/filterrule.html b/web/templates/setting/filterrule.html new file mode 100644 index 0000000..b310c07 --- /dev/null +++ b/web/templates/setting/filterrule.html @@ -0,0 +1,560 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + + +{% if Count > 0 %} + +{% else %} + {{ OOPS.empty('没有规则', '没有配置任何规则,请点击“新增规则组“按钮。') }} +{% endif %} + + + + + + \ No newline at end of file diff --git a/web/templates/setting/indexer.html b/web/templates/setting/indexer.html new file mode 100644 index 0000000..c038b3a --- /dev/null +++ b/web/templates/setting/indexer.html @@ -0,0 +1,192 @@ +{% import 'macro/form.html' as FORM %} +
+ + +
+ + +{% for Type, Indexer in IndexerConf.items() %} + +{% endfor %} + + \ No newline at end of file diff --git a/web/templates/setting/library.html b/web/templates/setting/library.html new file mode 100644 index 0000000..39bc91c --- /dev/null +++ b/web/templates/setting/library.html @@ -0,0 +1,238 @@ +{% import 'macro/svg.html' as SVG %} +
+ + +
+ +
+
+
+
+
+ +
+ + + + + + + + + {% if Config.media.movie_path %} + {% for path in Config.media.movie_path %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
目录
+ + {{ path }} + + + {{ SVG.x() }} + +
未配置
+
+
+
+
+
+
+

电视剧

+ + {{ SVG.plus() }} + +
+
+ + + + + + + + + {% if Config.media.tv_path %} + {% for path in Config.media.tv_path %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
目录
+ + {{ path }} + + + {{ SVG.x() }} + +
未配置
+
+
+
+
+
+ +
+ + + + + + + + + {% if Config.media.anime_path %} + {% for path in Config.media.anime_path %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
目录
+ + {{ path }} + + + {{ SVG.x() }} + +
未配置
+
+
+
+
+
+
+

未识别

+ + {{ SVG.plus() }} + +
+
+ + + + + + + + + {% if Config.media.unknown_path %} + {% for path in Config.media.unknown_path %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
目录
+ + {{ path }} + + + {{ SVG.x() }} + +
未配置
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/setting/mediaserver.html b/web/templates/setting/mediaserver.html new file mode 100644 index 0000000..6234503 --- /dev/null +++ b/web/templates/setting/mediaserver.html @@ -0,0 +1,100 @@ +{% import 'macro/form.html' as FORM %} +
+ + +
+ +
+
+
+ {% for Type, MediaServer in MediaServerConf.items() %} + +
+ + +
+
+
{{ MediaServer.name }}
+
{% if Config.media.media_server == Type %} + 正在使用{% endif %}
+
+
+ {% endfor %} +
+
+
+{% for Type, MediaServer in MediaServerConf.items() %} + +{% endfor %} + \ No newline at end of file diff --git a/web/templates/setting/notification.html b/web/templates/setting/notification.html new file mode 100644 index 0000000..33f14b8 --- /dev/null +++ b/web/templates/setting/notification.html @@ -0,0 +1,476 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/form.html' as FORM %} + + +
+
+
+
+
+
+
+ 共 {{ ClientCount }} 条记录 +
+
+
+
+ + + + + + + + + + + + + {% if MessageClients %} + {% for Id, Attr in MessageClients.items() %} + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
类型名称推送内容交互启用
+ + {{ Attr.name }} + + {% for swid in Attr.switchs %} + {{ Switchs[swid].name }} + {% endfor %} + + + {% if Channels[Attr.type].search_type %} + + {% endif %} + + + + +
未配置
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/web/templates/setting/subtitle.html b/web/templates/setting/subtitle.html new file mode 100644 index 0000000..7399749 --- /dev/null +++ b/web/templates/setting/subtitle.html @@ -0,0 +1,163 @@ +
+ + +
+ + + + + \ No newline at end of file diff --git a/web/templates/setting/users.html b/web/templates/setting/users.html new file mode 100644 index 0000000..b17bd8f --- /dev/null +++ b/web/templates/setting/users.html @@ -0,0 +1,193 @@ +{% import 'macro/svg.html' as SVG %} +
+ + +
+ +
+
+
+
+
+
+
+
+ 共 {{ UserCount }} 条记录 +
+
+
+
+ + + + + + + + + + {% if Users %} + {% for User in Users %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
用户名权限
{{ User.name or '' }} +
+ {% for pri in User.pris %} + {{ pri }} + {% endfor %} +
+
+ + {{ SVG.x() }} + +
没有数据
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/web/templates/site/brushtask.html b/web/templates/site/brushtask.html new file mode 100644 index 0000000..efdf44a --- /dev/null +++ b/web/templates/site/brushtask.html @@ -0,0 +1,1110 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + +{% if Count > 0 %} +
+
+
+ {% for Task in Tasks %} +
+ +
+
+
+
站点
+
+
+ +

{{ Task.site }}

+
+
+
+
+
促销
+
+ {% if Task.free %}{{ Task.free }} + {% else %} + 全部 + {% endif %} +
+
+
+
选种规则
+
+ {% if Task.rss_rule|brush_rule_string|safe %} + {{ Task.rss_rule|brush_rule_string|safe }} + {% else %} + 无限制 + {% endif %} +
+
+
+
删种规则
+
+ {% if Task.remove_rule|brush_rule_string|safe %} + {{ Task.remove_rule|brush_rule_string|safe }} + {% else %} + 未启用 + {% endif %} +
+
+
+
保种体积
+
+ {% if Task.seed_size %}{{ Task.seed_size }} GB{% else %}无限制{% endif %}
+
+
+
刷新间隔
+
{{ Task.interval }} 分钟
+
+
+
下载器
+
{{ Task.downloader_name or "" }}
+
+
+
消息推送
+
+ {% if Task.sendmessage == 'Y' %} + + {% else %} + + {% endif %} +
+
+
+
强制做种
+
+ {% if Task.forceupload == 'Y' %} + + {% else %} + + {% endif %} +
+
+
+
转移到媒体库
+
+ {% if Task.transfer == 'Y' %} + + {% else %} + + {% endif %} +
+
+
+
已下载种子数
+
{{ Task.download_count }}
+
+
+
已删除种子数
+
{{ Task.remove_count }}
+
+
+
下载量
+
{{ Task.download_size }}
+
+
+
上传量
+
{{ Task.upload_size }}
+
+
+
最后更新时间
+
+ {{ Task.lst_mod_date }} +
+
+
+
状态
+
+ {% if Task.state == 'Y' %} + 正在运行 + {% else %} + 已停用 + {% endif %} +
+
+
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有任务', '当前没有正在运行的刷流任务。') }} +{% endif %} + + + \ No newline at end of file diff --git a/web/templates/site/resources.html b/web/templates/site/resources.html new file mode 100644 index 0000000..39c264b --- /dev/null +++ b/web/templates/site/resources.html @@ -0,0 +1,290 @@ +{% import 'macro/svg.html' as SVG %} + +
+
+
+
+
+
+
+
+ 共 {{ TotalCount }} 条记录 +
+
+ 搜索: +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + {% if TotalCount > 0 %} + {% for Result in Results %} + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + 标题 + + + + + + + + + +
+ + + + + + + + + + +
+ {{ Result.title }} + {% if Result.uploadvolumefactor != 1.0 %} + {{ (Result.uploadvolumefactor * 100) | int }}%UL + {% endif %} + {% if Result.downloadvolumefactor != 1.0 %} + {% if Result.downloadvolumefactor == 0.0 %} + FREE + {% else %} + {{ (Result.downloadvolumefactor * 100) | int }}%DL + {% endif %} + {% endif %} + + {{ SVG.text_recognition() }} + +
+
+ {% if Result.imdbid %} + + {% endif %} + {{ Result.description }} +
+ +
{{ Result.date_elapsed }} + {{ Result.size|str_filesize }} + {{ Result.seeders }}{{ Result.peers }}{{ Result.grabs }} + + {{ SVG.download() }} + +
没有数据
+
+ {% if TotalCount > 0 and not KeyWord %} + + {% endif %} +
+
+
+
+
+ \ No newline at end of file diff --git a/web/templates/site/site.html b/web/templates/site/site.html new file mode 100644 index 0000000..b0f5230 --- /dev/null +++ b/web/templates/site/site.html @@ -0,0 +1,906 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} + +{% if Sites %} +
+
+
+ {% for Site in Sites %} + + {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有站点', '没有添加任何站点,请点击”新增站点“按钮。') }} +{% endif %} + + + + + + \ No newline at end of file diff --git a/web/templates/site/sitelist.html b/web/templates/site/sitelist.html new file mode 100644 index 0000000..1c0fa20 --- /dev/null +++ b/web/templates/site/sitelist.html @@ -0,0 +1,47 @@ +{% import 'macro/oops.html' as OOPS %} +
+ + +
+ +{% if Count > 0 %} +
+
+
+ {% for Site in Sites %} + +
+
+
+ +
+
+
{{ Site.name }}
+
{{ Site.domain }}
+
+
+
+
+ {% endfor %} +
+
+
+{% else %} +{{ OOPS.nodatafound('没有站点', '没有找到任何站点,请正确维护站点信息。') }} +{% endif %} + \ No newline at end of file diff --git a/web/templates/site/statistics.html b/web/templates/site/statistics.html new file mode 100644 index 0000000..a4da53c --- /dev/null +++ b/web/templates/site/statistics.html @@ -0,0 +1,1067 @@ +{% import 'macro/svg.html' as SVG %} +{% import 'macro/oops.html' as OOPS %} +
+ +
+{% if SiteNames %} +
+
+
+
+
+
+
+
+ + {{ SVG.world_upload() }} + +
+
+
+
总上传量
+
+
+
{{ TotalUpload | filesizeformat(true) }}
+
+
+
+
+
+
+
+
+
+
+
+ + {{ SVG.world_download() }} + +
+
+
+
总下载量
+
+
+
{{ TotalDownload | filesizeformat(true) }}
+
+
+
+
+
+
+
+
+
+
+
+ + {{ SVG.arrow_big_up_lines() }} + +
+
+
+
总做种数
+
+
+
{{ TotalSeeding }}
+
+
+
+
+
+
+
+
+
+
+
+ + {{ SVG.cloud_upload() }} + +
+
+
+
总做种体积
+
+
+
{{ TotalSeedingSize | filesizeformat(true) }}
+
+
+
+
+
+
+
+
+
+
+
+
+

今日上传量 共{{ CurrentUpload | filesizeformat(true) }}

+
+
+
+
+
+
+
+
+
+

今日下载量 共{{ CurrentDownload | filesizeformat(true) }}

+
+
+
+
+
+
+
+
+
+

历史数据

+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+

站点数据

+ {% if SiteUserStatistics | count > 0 %} + + {% endif %} +
+ 共 {{ SiteUserStatistics | count }} 条记录 +
+ 0 %}onclick="navmenu('statistics?refresh_force=1')"{% endif %}> + {{ SVG.refresh() }} + +
+
+
+ + + + + + + + + + + + + + + + + {% if SiteUserStatistics | count > 0 %} + {% for item in SiteUserStatistics %} + + + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
+ + 等级 + + + + + + + + + +
+
+ {% if SiteErr.get(item.site) %} + + {% else %} + + {% endif %} + +
+
{{ item.site }} + {% if item.msg_unread > 0 %} + {{ SVG.message() }} + {% endif %} +
+
{{ item.username }}
+
+
+
+ {{ item.user_level }} + +
+ {{ SVG.arrow_narrow_up() }} + {{ item.upload | filesizeformat(true) }} +
+
+ {{ SVG.arrow_narrow_down() }} + {{ item.download | filesizeformat(true) }} +
+
+ {% if item.ratio == 0.0 or item.ratio > 10000 %}Infinity{% else %} + {{ '%.2f' | format(item.ratio) }}{% endif %} + {{ item.seeding }}{{ item.seeding_size | filesizeformat(true) }}{{ item.bonus }} + {{ item.join_at | string | truncate(10, True, '') }} + + {{ item.update_at }} + + + {{ SVG.activity() }} + +
没有数据
+
+
+
+
+
+
+ + + + + + +{% else %} +{{ OOPS.nodatafound('没有数据', '没有生成站点统计数据,请确认是否正确配置站点信息。') }} +{% endif %} \ No newline at end of file diff --git a/web/templates/test.html b/web/templates/test.html new file mode 100644 index 0000000..bbe24b9 --- /dev/null +++ b/web/templates/test.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + 组件开发效果预览 + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ +
+
+ + \ No newline at end of file diff --git a/windows/nas-tools.ico b/windows/nas-tools.ico new file mode 100644 index 0000000..bca7b40 Binary files /dev/null and b/windows/nas-tools.ico differ diff --git a/windows/nas-tools.spec b/windows/nas-tools.spec new file mode 100644 index 0000000..85638b1 --- /dev/null +++ b/windows/nas-tools.spec @@ -0,0 +1,121 @@ +# -*- mode: python -*- + +# <<< START ADDED PART +from PyInstaller.building.build_main import Analysis, PYZ, EXE, COLLECT, BUNDLE, TOC + + +def collect_pkg_data(package, include_py_files=False, subdir=None): + import os + from PyInstaller.utils.hooks import get_package_paths, remove_prefix, PY_IGNORE_EXTENSIONS + + # Accept only strings as packages. + if type(package) is not str: + raise ValueError + + pkg_base, pkg_dir = get_package_paths(package) + if subdir: + pkg_dir = os.path.join(pkg_dir, subdir) + # Walk through all file in the given package, looking for data files. + data_toc = TOC() + for dir_path, dir_names, files in os.walk(pkg_dir): + for f in files: + extension = os.path.splitext(f)[1] + if include_py_files or (extension not in PY_IGNORE_EXTENSIONS): + source_file = os.path.join(dir_path, f) + dest_folder = remove_prefix(dir_path, os.path.dirname(pkg_base) + os.sep) + dest_file = os.path.join(dest_folder, f) + data_toc.append((dest_file, source_file, 'DATA')) + + return data_toc + +pkg_data1 = collect_pkg_data('web') +pkg_data2 = collect_pkg_data('config') +pkg_data3 = collect_pkg_data('db_scripts', include_py_files=True) # <<< Put the name of your package here +# <<< END ADDED PART + + +# <<< START PATHEX PART +pathex_tp = [] +with open("third_party.txt") as third_party: + for third_party_lib in third_party: + pathex_tp.append(('./../third_party/' + third_party_lib).replace("\n", "")) +# <<< END PATHEX PART + +# <<< START HIDDENIMPORTS PART +def collect_local_submodules(package): + import os + base_dir = '..' + package_dir= os.path.join(base_dir, package.replace('.', os.sep)) + submodules = [] + for dir_path, dir_names, files in os.walk(package_dir): + for f in files: + if f == '__init__.py': + continue + if f.endswith('.py'): + submodules.append(package + '.' + f[:-3]) + return submodules + +hiddenimports = ['Crypto.Math', + 'Crypto.Cipher', + 'Crypto.Util', + 'Crypto.Hash', + 'Crypto.Protocol', + 'app.sites.siteuserinfo', + 'app.mediaserver.client', + 'app.message.client', + 'app.indexer.client', + 'app.downloader.client', + 'app.sites.sitesignin'] +hiddenimports += collect_local_submodules('app.sites.siteuserinfo') +hiddenimports += collect_local_submodules('app.mediaserver.client') +hiddenimports += collect_local_submodules('app.message.client') +hiddenimports += collect_local_submodules('app.indexer.client') +hiddenimports += collect_local_submodules('app.downloader.client') +hiddenimports += collect_local_submodules('app.sites.sitesignin') +# <<< END HIDDENIMPORTS PART + +block_cipher = None + + +a = Analysis( + ['./../run.py'], + pathex=pathex_tp, + binaries=[], + datas=[], + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +a.datas += [('./nas-tools.ico', './nas-tools.ico', 'DATA')] +a.datas += [('./third_party.txt', './third_party.txt', 'DATA')] +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + pkg_data1, + pkg_data2, + pkg_data3, + [], + name='nas-tools', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='nas-tools.ico' +) diff --git a/windows/rely/hook-cn2an.py b/windows/rely/hook-cn2an.py new file mode 100644 index 0000000..5ebbaa5 --- /dev/null +++ b/windows/rely/hook-cn2an.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files("cn2an") diff --git a/windows/rely/hook-zhconv.py b/windows/rely/hook-zhconv.py new file mode 100644 index 0000000..be7e3a9 --- /dev/null +++ b/windows/rely/hook-zhconv.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files("zhconv") diff --git a/windows/rely/template.jinja2 b/windows/rely/template.jinja2 new file mode 100644 index 0000000..d2219fb --- /dev/null +++ b/windows/rely/template.jinja2 @@ -0,0 +1,26 @@ + + + + {% if not head %} + + {% else %} + {{ hear | safe }} + {% endif %} + + +{{ body | safe }} +{% for diagram in diagrams %} +
+

{{ diagram.title }}

+
{{ diagram.text }}
+
+ {{ diagram.svg }} +
+
+{% endfor %} + + diff --git a/windows/rely/upx.exe b/windows/rely/upx.exe new file mode 100644 index 0000000..436082b Binary files /dev/null and b/windows/rely/upx.exe differ diff --git a/windows/trayicon.py b/windows/trayicon.py new file mode 100644 index 0000000..063fd9c --- /dev/null +++ b/windows/trayicon.py @@ -0,0 +1,69 @@ +import os +import sys +import webbrowser + +import wx +import wx.adv + + +class Balloon(wx.adv.TaskBarIcon): + ICON = os.path.dirname(__file__).replace("windows", "") + "nas-tools.ico" + + def __init__(self, homepage, log_path): + wx.adv.TaskBarIcon.__init__(self) + self.SetIcon(wx.Icon(self.ICON)) + self.Bind(wx.adv.EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBarLeftDClick) + self.homepage = homepage + self.log_path = log_path + + # Menu数据 + def setMenuItemData(self): + return ("Log", self.Onlog), ("Close", self.OnClose) + + # 创建菜单 + def CreatePopupMenu(self): + menu = wx.Menu() + for itemName, itemHandler in self.setMenuItemData(): + if not itemName: # itemName为空就添加分隔符 + menu.AppendSeparator() + continue + menuItem = wx.MenuItem(None, wx.ID_ANY, text=itemName, kind=wx.ITEM_NORMAL) # 创建菜单项 + menu.Append(menuItem) # 将菜单项添加到菜单 + self.Bind(wx.EVT_MENU, itemHandler, menuItem) + return menu + + def OnTaskBarLeftDClick(self, event): + webbrowser.open(self.homepage) + + def Onlog(self, event): + os.startfile(self.log_path) + + @staticmethod + def OnClose(event): + exe_name = os.path.basename(sys.executable) + os.system('taskkill /F /IM ' + exe_name) + + +class TrayIcon(wx.Frame): + def __init__(self, homepage, log_path): + app = wx.App() + wx.Frame.__init__(self, None) + self.taskBarIcon = Balloon(homepage, log_path) + webbrowser.open(homepage) + self.Hide() + app.MainLoop() + + +class NullWriter: + softspace = 0 + encoding = 'UTF-8' + + def write(*args): + pass + + def flush(*args): + pass + + # Some packages are checking if stdout/stderr is available (e.g., youtube-dl). For details, see #1883. + def isatty(self): + return False